OpenGL入门学习——第十五课,从“绘制一个立方体”来看OpenGL的进化过程

点击下载附件
现在即将放出的是第十五课的内容。 首先还是以前课程的连接: 第一课,编写第一个OpenGL程序 第二课,绘制几何图形 第三课,绘制几何图形的一些细节问题 第四课,颜色的选择 第五课,三维的空间变换 第六课,动画的制作 第七课,使用光照来表现立体感 第八课,使用显示列表 第九课,使用混合来实现半透明效果 第十课,BMP文件与像素操作 第十一课,纹理的使用入门 第十二课,OpenGL片断测试 第十三课,OpenGL是一个状态机 第十四课,OpenGL版本和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]); [...]

News, OpenGL, Tutorial

OpenGL入门学习——第十四课,OpenGL版本和OpenGL扩展

点击下载附件
现在即将放出的是第十四课的内容。 首先还是以前课程的连接: 第一课,编写第一个OpenGL程序 第二课,绘制几何图形 第三课,绘制几何图形的一些细节问题 第四课,颜色的选择 第五课,三维的空间变换 第六课,动画的制作 第七课,使用光照来表现立体感 第八课,使用显示列表 第九课,使用混合来实现半透明效果 第十课,BMP文件与像素操作 第十一课,纹理的使用入门 第十二课,OpenGL片断测试 第十三课,OpenGL是一个状态机 第十四课,OpenGL版本和OpenGL扩展 –→ 本次课程的内容 这次要讲的是OpenGL版本和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 [...]

OpenGL, Tutorial, opengl es

OpenGL入门学习——第十三课,OpenGL是一个状态机

点击下载附件
这个教程,拖了大半年都没有更新,很多朋友可能都以为它不会再更新了。有的时候甚至我自己也是这样认为的,因为水平的限制,还有时间的限制。但是我觉得,如果有机会能够写一点了,那么我就抓住机会写一点,总比什么都不写要好。 这个五一节没有什么安排,就多写一些课程给大家吧。 必须说明一下,我自己的水平实在有限,出错在所难免。前面写的十二课教程,有些内容的说明是不完整的或者不正确的。有的问题,朋友拿来问我,结果我自己都没办法自圆其说,实在惭愧。虽然现在自己的能力比写课程的时候有所提高,但是我也并没有修改那些教程了。 现在即将放出的是第十三课的内容。 首先还是以前课程的连接: 第一课,编写第一个OpenGL程序 第二课,绘制几何图形 第三课,绘制几何图形的一些细节问题 第四课,颜色的选择 第五课,三维的空间变换 第六课,动画的制作 第七课,使用光照来表现立体感 第八课,使用显示列表 第九课,使用混合来实现半透明效果 第十课,BMP文件与像素操作 第十一课,纹理的使用入门 第十二课,OpenGL片断测试 第十三课,OpenGL是一个状态机 –→ 本次课程的内容 提示:附件中有本次课程的全部内容,课程正文为HTML格式,图片通常为PNG格式,帖子中的图片为GIF或JPG格式,质量会稍差。 提示:本课的内容是枯燥的理论知识。如果读者能够理解,当然是好事,在以后的学习中会更加方便。如果暂时没能理解,也没有关系,可以跳过本课往后看。 接下来让我们进入正题。
前一段时间里,论坛有位朋友问什么是状态机。按我的理解,状态机就是一种存在于理论中的机器,它具有以下的特点: 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函数重新指定。 …… [...]

News, OpenGL, Tutorial

OpenGL入门学习——第十二课,OpenGL片断测试

点击下载附件
大家好。现在因为参加工作的关系,又是长时间没有更新。趁着国庆的空闲,总算是又写出了一课。我感觉入门的知识已经快要介绍完毕,这课之后再有一课,就可以告一段落了。以后我可能会写一些自己在这方面的体会,做一份进阶课程。 现在即将放出的是第十二课的内容。 首先还是以前课程的连接: 第一课,编写第一个OpenGL程序 第二课,绘制几何图形 第三课,绘制几何图形的一些细节问题 第四课,颜色的选择 第五课,三维的空间变换 第六课,动画的制作 第七课,使用光照来表现立体感 第八课,使用显示列表 第九课,使用混合来实现半透明效果 第十课,BMP文件与像素操作 第十一课,纹理的使用入门 第十二课,OpenGL片断测试 –→ 本次课程的内容 片断测试其实就是测试每一个像素,只有通过测试的像素才会被绘制,没有通过测试的像素则不进行绘制。OpenGL提供了多种测试操作,利用这些操作可以实现一些特殊的效果。 我们在前面的课程中,曾经提到了”深度测试”的概念,它在绘制三维场景的时候特别有用。在不使用深度测试的时候,如果我们先绘制一个距离较近的物体,再绘制距离较远的物体,则距离远的物体因为后绘制,会把距离近的物体覆盖掉,这样的效果并不是我们所希望的。 如果使用了深度测试,则情况就会有所不同:每当一个像素被绘制,OpenGL就记录这个像素的”深度”(深度可以理解为:该像素距离观察者的距离。深度值越大,表示距离越远),如果有新的像素即将覆盖原来的像素时,深度测试会检查新的深度是否会比原来的深度值小。如果是,则覆盖像素,绘制成功;如果不是,则不会覆盖原来的像素,绘制被取消。这样一来,即使我们先绘制比较近的物体,再绘制比较远的物体,则远的物体也不会覆盖近的物体了。 实际上,只要存在深度缓冲区,无论是否启用深度测试,OpenGL在像素被绘制时都会尝试将深度数据写入到缓冲区内,除非调用了glDepthMask(GL_FALSE)来禁止写入。这些深度数据除了用于常规的测试外,还可以有一些有趣的用途,比如绘制阴影等等。 除了深度测试,OpenGL还提供了剪裁测试、Alpha测试和模板测试。 因为论坛开始支持附件,现在把程序源代码和所使用的图片一起打包上传
1、剪裁测试 剪裁测试用于限制绘制区域。我们可以指定一个矩形的剪裁窗口,当启用剪裁测试后,只有在这个窗口之内的像素才能被绘制,其它像素则会被丢弃。换句话说,无论怎么绘制,剪裁窗口以外的像素将不会被修改。 有的朋友可能玩过《魔兽争霸3》这款游戏。游戏时如果选中一个士兵,则画面下方的一个方框内就会出现该士兵的头像。为了保证该头像无论如何绘制都不会越界而覆盖到外面的像素,就可以使用剪裁测试。 可以通过下面的代码来启用或禁用剪裁测试:
glEnable(GL_SCISSOR_TEST); // 启用剪裁测试 glDisable(GL_SCISSOR_TEST); // 禁用剪裁测试
可以通过下面的代码来指定一个位置在(x, y),宽度为width,高度为height的剪裁窗口。
glScissor(x, y, width, height);
注意,OpenGL窗口坐标是以左下角为(0, 0),右上角为(width, height)的,这与Windows系统窗口有所不同。 还有一种方法可以保证像素只绘制到某一个特定的矩形区域内,这就是视口变换(在第五课第3节中有介绍)。但视口变换和剪裁测试是不同的。视口变换是将所有内容缩放到合适的大小后,放到一个矩形的区域内;而剪裁测试不会进行缩放,超出矩形范围的像素直接忽略掉。
2、Alpha测试 在前面的课程中,我们知道像素的Alpha值可以用于混合操作。其实Alpha值还有一个用途,这就是Alpha测试。当每个像素即将绘制时,如果启动了Alpha测试,OpenGL会检查像素的Alpha值,只有 Alpha值满足条件的像素才会进行绘制(严格的说,满足条件的像素会通过本项测试,进行下一种测试,只有所有测试都通过,才能进行绘制),不满足条件的则不进行绘制。这个”条件”可以是:始终通过(默认情况)、始终不通过、大于设定值则通过、小于设定值则通过、等于设定值则通过、大于等于设定值则通过、小于等于设定值则通过、不等于设定值则通过。 如果我们需要绘制一幅图片,而这幅图片的某些部分又是透明的(想象一下,你先绘制一幅相片,然后绘制一个相框,则相框这幅图片有很多地方都是透明的,这样就可以透过相框看到下面的照片),这时可以使用Alpha测试。将图片中所有需要透明的地方的 Alpha值设置为0.0,不需要透明的地方Alpha值设置为1.0,然后设置Alpha测试的通过条件为:”大于0.5则通过”,这样便能达到目的。当然也可以设置需要透明的地方Alpha值为1.0,不需要透明的地方Alpha值设置为0.0,然后设置条件为”小于0.5则通过”。Alpha测试的设置方式往往不只一种,可以根据个人喜好和实际情况需要进行选择。 可以通过下面的代码来启用或禁用Alpha测试:
glEnable(GL_ALPHA_TEST); // 启用Alpha测试 glDisable(GL_ALPHA_TEST); // 禁用Alpha测试
可以通过下面的代码来设置Alpha测试条件为”大于0.5则通过”:
glAlphaFunc(GL_GREATER, 0.5f);
该函数的第二个参数表示设定值,用于进行比较。第一个参数是比较方式,除了GL_LESS(小于则通过)外,还可以选择: GL_ALWAYS(始终通过), [...]

News, OpenGL, Tutorial

OpenGL入门学习——第八课

现在趁着有点空闲,更新快一点。 第一课,编写第一个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函数判断一个编号是否已经被用作显示列表。 二、创建显示列表 [...]

News, OpenGL, Tutorial

OpenGL入门学习——第七课

沉默了好长一段时间,这个课程终于要开始更新了。不是我不想写,实在是因为前段时间忙着干一些无聊却又不得不做的事情。 好了,我也不在这里发什么牢骚,下面先回顾一下已经学习的课程: 第一课,编写第一个OpenGL程序 第二课,绘制几何图形 第三课,绘制几何图形的一些细节问题 第四课,颜色的选择 第五课,三维的空间变换 第六课,动画的制作 第七课,使用光照来表现立体感 –→ 本次课程的内容 今天要讲的是OpenGL光照的基本知识。虽然内容显得有点多,但条理还算比较清晰,理解起来应该没有困难。即使对于一些内容没有记住,问题也不大–光照部分是一个比较独立的内容,它的学习与其它方面的学习可以分开,不像视图变换那样,影响到许多方面。课程的最后给出了一个有关光照效果的动画演示程序,我想大家会喜欢的。 从生理学的角度上讲,眼睛之所以看见各种物体,是因为光线直接或间接的从它们那里到达了眼睛。人类对于光线强弱的变化的反应,比对于颜色变化的反应来得灵敏。因此对于人类而言,光线很大程度上表现了物体的立体感。 请看图1,图中绘制了两个大小相同的白色球体。其中右边的一个是没有使用任何光照效果的,它看起来就像是一个二维的圆盘,没有立体的感觉。左边的一个是使用了简单的光照效果的,我们通过光照的层次,很容易的认为它是一个三维的物体。 图1 OpenGL对于光照效果提供了直接的支持,只需要调用某些函数,便可以实现简单的光照效果。但是在这之前,我们有必要了解一些基础知识。 一、建立光照模型 在现实生活中,某些物体本身就会发光,例如太阳、电灯等,而其它物体虽然不会发光,但可以反射来自其它物体的光。这些光通过各种方式传播,最后进入我们的眼睛–于是一幅画面就在我们的眼中形成了。 就目前的计算机而言,要准确模拟各种光线的传播,这是无法做到的事情。比如一个四面都是粗糙墙壁的房间,一盏电灯所发出的光线在很短的时间内就会经过非常多次的反射,最终几乎布满了房间的每一个角落,这一过程即使使用目前运算速度最快的计算机,也无法精确模拟。不过,我们并不需要精确的模拟各种光线,只需要找到一种近似的计算方式,使它的最终结果让我们的眼睛认为它是真实的,这就可以了。 OpenGL在处理光照时采用这样一种近似:把光照系统分为三部分,分别是光源、材质和光照环境。光源就是光的来源,可以是前面所说的太阳或者电灯等。材质是指接受光照的各种物体的表面,由于物体如何反射光线只由物体表面决定(OpenGL中没有考虑光的折射),材质特点就决定了物体反射光线的特点。光照环境是指一些额外的参数,它们将影响最终的光照画面,比如一些光线经过多次反射后,已经无法分清它究竟是由哪个光源发出,这时,指定一个”环境亮度”参数,可以使最后形成的画面更接近于真实情况。 在物理学中,光线如果射入理想的光滑平面,则反射后的光线是很规则的(这样的反射称为镜面反射)。光线如果射入粗糙的、不光滑的平面,则反射后的光线是杂乱的(这样的反射称为漫反射)。现实生活中的物体在反射光线时,并不是绝对的镜面反射或漫反射,但可以看成是这两种反射的叠加。对于光源发出的光线,可以分别设置其经过镜面反射和漫反射后的光线强度。对于被光线照射的材质,也可以分别设置光线经过镜面反射和漫反射后的光线强度。这些因素综合起来,就形成了最终的光照效果。 二、法线向量 根据光的反射定律,由光的入射方向和入射点的法线就可以得到光的出射方向。因此,对于指定的物体,在指定了光源后,即可计算出光的反射方向,进而计算出光照效果的画面。在OpenGL中,法线的方向是用一个向量来表示。 不幸的是,OpenGL并不会根据你所指定的多边形各个顶点来计算出这些多边形所构成的物体的表面的每个点的法线(这话听着有些迷糊),通常,为了实现光照效果,需要在代码中为每一个顶点指定其法线向量。 指定法线向量的方式与指定颜色的方式有雷同之处。在指定颜色时,只需要指定每一个顶点的颜色,OpenGL就可以自行计算顶点之间的其它点的颜色。并且,颜色一旦被指定,除非再指定新的颜色,否则以后指定的所有顶点都将以这一向量作为自己的颜色。在指定法线向量时,只需要指定每一个顶点的法线向量,OpenGL会自行计算顶点之间的其它点的法线向量。并且,法线向量一旦被指定,除非再指定新的法线向量,否则以后指定的所有顶点都将以这一向量作为自己的法线向量。使用glColor*函数可以指定颜色,而使用glNormal*函数则可以指定法线向量。 注意:使用glTranslate*函数或者glRotate*函数可以改变物体的外观,但法线向量并不会随之改变。然而,使用glScale*函数,对每一坐标轴进行不同程度的缩放,很有可能导致法线向量的不正确,虽然OpenGL提供了一些措施来修正这一问题,但由此也带来了各种开销。因此,在使用了法线向量的场合,应尽量避免使用glScale*函数。即使使用,也最好保证各坐标轴进行等比例缩放。 三、控制光源 在OpenGL中,仅仅支持有限数量的光源。使用GL_LIGHT0表示第0号光源,GL_LIGHT1表示第1号光源,依次类推,OpenGL至少会支持8个光源,即GL_LIGHT0到GL_LIGHT7。使用glEnable函数可以开启它们。例如,glEnable(GL_LIGHT0);可以开启第0号光源。使用glDisable函数则可以关闭光源。一些OpenGL实现可能支持更多数量的光源,但总的来说,开启过多的光源将会导致程序运行速度的严重下降,玩过3D Mark的朋友可能多少也有些体会。一些场景中可能有成百上千的电灯,这时可能需要采取一些近似的手段来进行编程,否则以目前的计算机而言,是无法运行这样的程序的。 每一个光源都可以设置其属性,这一动作是通过glLight*函数完成的。glLight*函数具有三个参数,第一个参数指明是设置哪一个光源的属性,第二个参数指明是设置该光源的哪一个属性,第三个参数则是指明把该属性值设置成多少。光源的属性众多,下面将分别介绍。 (1)GL_AMBIENT、GL_DIFFUSE、GL_SPECULAR属性。这三个属性表示了光源所发出的光的反射特性(以及颜色)。每个属性由四个值表示,分别代表了颜色的R, G, B, A值。GL_AMBIENT表示该光源所发出的光,经过非常多次的反射后,最终遗留在整个光照环境中的强度(颜色)。GL_DIFFUSE表示该光源所发出的光,照射到粗糙表面时经过漫反射,所得到的光的强度(颜色)。GL_SPECULAR表示该光源所发出的光,照射到光滑表面时经过镜面反射,所得到的光的强度(颜色)。 (2)GL_POSITION属性。表示光源所在的位置。由四个值(X, Y, Z, W)表示。如果第四个值W为零,则表示该光源位于无限远处,前三个值表示了它所在的方向。这种光源称为方向性光源,通常,太阳可以近似的被认为是方向性光源。如果第四个值W不为零,则X/W, Y/W, Z/W表示了光源的位置。这种光源称为位置性光源。对于位置性光源,设置其位置与设置多边形顶点的方式相似,各种矩阵变换函数例如:glTranslate*、glRotate*等在这里也同样有效。方向性光源在计算时比位置性光源快了不少,因此,在视觉效果允许的情况下,应该尽可能的使用方向性光源。 (3)GL_SPOT_DIRECTION、GL_SPOT_EXPONENT、GL_SPOT_CUTOFF属性。表示将光源作为聚光灯使用(这些属性只对位置性光源有效)。很多光源都是向四面八方发射光线,但有时候一些光源则是只向某个方向发射,比如手电筒,只向一个较小的角度发射光线。GL_SPOT_DIRECTION属性有三个值,表示一个向量,即光源发射的方向。GL_SPOT_EXPONENT属性只有一个值,表示聚光的程度,为零时表示光照范围内向各方向发射的光线强度相同,为正数时表示光照向中央集中,正对发射方向的位置受到更多光照,其它位置受到较少光照。数值越大,聚光效果就越明显。GL_SPOT_CUTOFF属性也只有一个值,表示一个角度,它是光源发射光线所覆盖角度的一半(见图2),其取值范围在0到90之间,也可以取180这个特殊值。取值为180时表示光源发射光线覆盖360度,即不使用聚光灯,向全周围发射。 图2 (4)GL_CONSTANT_ATTENUATION、GL_LINEAR_ATTENUATION、GL_QUADRATIC_ATTENUATION属性。这三个属性表示了光源所发出的光线的直线传播特性(这些属性只对位置性光源有效)。现实生活中,光线的强度随着距离的增加而减弱,OpenGL把这个减弱的趋势抽象成函数: 衰减因子 = 1 / (k1 + k2 * d + [...]

News, OpenGL, Tutorial

OpenGL入门学习——第六课

趁着国庆这把火,再写一课吧。要不过两天,事情就又多起来了。 今后一段时间内我极有可能暂停更新,希望大家做好心理准备 。学习是不能停止的,大家加油了。 下面回顾一下我们已经学习的课程: 第一课,编写第一个OpenGL程序 第二课,绘制几何图形 第三课,绘制几何图形的一些细节问题 第四课,颜色的选择 第五课,三维的空间变换 第六课,动画的制作 –→ 本次课程的内容 今天要讲的是动画制作–可能是各位都很喜欢的。除了讲授知识外,我们还会让昨天那个”太阳、地球和月亮”天体图画动起来。缓和一下枯燥的气氛。
本次课程,我们将进入激动人心的计算机动画世界。 想必大家都知道电影和动画的工作原理吧?是的,快速的把看似连续的画面一幅幅的呈现在人们面前。一旦每秒钟呈现的画面超过24幅,人们就会错以为它是连续的。 我们通常观看的电视,每秒播放25或30幅画面。但对于计算机来说,它可以播放更多的画面,以达到更平滑的效果。如果速度过慢,画面不够平滑。如果速度过快,则人眼未必就能反应得过来。对于一个正常人来说,每秒60~120幅图画是比较合适的。具体的数值因人而异。 假设某动画一共有n幅画面,则它的工作步骤就是: 显示第1幅画面,然后等待一小段时间,直到下一个1/24秒 显示第2幅画面,然后等待一小段时间,直到下一个1/24秒 …… 显示第n幅画面,然后等待一小段时间,直到下一个1/24秒 结束 如果用C语言伪代码来描述这一过程,就是: for(i=0; i<n; ++i) { DrawScene(i); Wait(); }
1、双缓冲技术 在计算机上的动画与实际的动画有些不同:实际的动画都是先画好了,播放的时候直接拿出来显示就行。计算机动画则是画一张,就拿出来一张,再画下一张,再拿出来。如果所需要绘制的图形很简单,那么这样也没什么问题。但一旦图形比较复杂,绘制需要的时间较长,问题就会变得突出。 让我们把计算机想象成一个画图比较快的人,假如他直接在屏幕上画图,而图形比较复杂,则有可能在他只画了某幅图的一半的时候就被观众看到。而后面虽然他把画补全了,但观众的眼睛却又没有反应过来,还停留在原来那个残缺的画面上。也就是说,有时候观众看到完整的图象,有时却又只看到残缺的图象,这样就造成了屏幕的闪烁。 如何解决这一问题呢?我们设想有两块画板,画图的人在旁边画,画好以后把他手里的画板与挂在屏幕上的画板相交换。这样以来,观众就不会看到残缺的画了。这一技术被应用到计算机图形中,称为双缓冲技术。即:在存储器(很有可能是显存)中开辟两块区域,一块作为发送到显示器的数据,一块作为绘画的区域,在适当的时候交换它们。由于交换两块内存区域实际上只需要交换两个指针,这一方法效率非常高,所以被广泛的采用。 注意:虽然绝大多数平台都支持双缓冲技术,但这一技术并不是OpenGL标准中的内容。OpenGL为了保证更好的可移植性,允许在实现时不使用双缓冲技术。当然,我们常用的PC都是支持双缓冲技术的。 要启动双缓冲功能,最简单的办法就是使用GLUT工具包。我们以前在main函数里面写: glutInitDisplayMode(GLUT_RGB | GLUT_SINGLE); 其中GLUT_SINGLE表示单缓冲,如果改成GLUT_DOUBLE就是双缓冲了。 当然还有需要更改的地方–每次绘制完成时,我们需要交换两个缓冲区,把绘制好的信息用于屏幕显示(否则无论怎么绘制,还是什么都看不到)。如果使用GLUT工具包,也可以很轻松的完成这一工作,只要在绘制完成时简单的调用glutSwapBuffers函数就可以了。
2、实现连续动画 似乎没有任何疑问,我们应该把绘制动画的代码写成下面这个样子: for(i=0; i<n; ++i) { DrawScene(i); glutSwapBuffers(); Wait(); } 但事实上,这样做不太符合窗口系统的程序设计思路。还记得我们的第一个OpenGL程序吗?我们在main函数里写:glutDisplayFunc(&myDisplay); 意思是对系统说:如果你需要绘制窗口了,请调用myDisplay这个函数。为什么我们不直接调用myDisplay,而要采用这种看似”舍近求远”的做法呢?原因在于–我们自己的程序无法掌握究竟什么时候该绘制窗口。因为一般的窗口系统–拿我们熟悉一点的来说–Windows和X窗口系统,都是支持同时显示多个窗口的。假如你的程序窗口碰巧被别的窗口遮住了,后来用户又把原来遮住的窗口移开,这时你的窗口需要重新绘制。很不幸的,你无法知道这一事件发生的具体时间。因此这一切只好委托操作系统来办了。 现在我们再看上面那个循环。既然DrawScene都可以交给操作系统来代办了,那让整个循环运行起来的工作是否也可以交给操作系统呢?答案是肯定的。我们先前的思路是:绘制,然后等待一段时间;再绘制,再等待一段时间。但如果去掉等待的时间,就变成了绘制,绘制,……,不停的绘制。–当然了,资源是公用的嘛,杀毒软件总要工作吧?我的下载不能停下来吧?我的mp3播放还不能给耽搁了。总不能因为我们的动画,让其他的工作都停下来。因此,我们需要在CPU空闲的时间绘制。 这里的”在CPU空闲的时间绘制”和我们在第一课讲的”在需要绘制的时候绘制”有些共通,都是”在XX时间做XX事”,GLUT工具包也提供了一个比较类似的函数:glutIdleFunc,表示在CPU空闲的时间调用某一函数。其实GLUT还提供了一些别的函数,例如”在键盘按下时做某事”等。
到现在,我们已经可以初步开始制作动画了。好的,就拿上次那个”太阳、地球和月亮”的程序开刀,让地球和月亮自己动起来。
#include <GL/glut.h> // [...]

News, OpenGL, Tutorial

OpenGL入门学习——第五课

大家久等了。 不知道是否还有人记得这个课程啊?下面是前面四课的连接,大家可以温习一下,新朋友也可以看看。 第一课,编写第一个OpenGL程序 第二课,绘制几何图形 第三课,绘制几何图形的一些细节问题 第四课,颜色的选择 第五课,三维的空间变换 –→ 本次课程的内容 今天要讲的是三维变换的内容,课程比较枯燥。主要是因为很多函数在单独使用时都不好描述其效果,我只好在最后举一个比较综合的例子。希望大家能一口气看到底了。只看一次可能不够,如果感觉到迷糊,不妨多看两遍。有疑问可以在下面跟帖提出。 我也使用了若干图形,希望可以帮助理解。
在前面绘制几何图形的时候,大家是否觉得我们绘图的范围太狭隘了呢?坐标只能从-1到1,还只能是X轴向右,Y轴向上,Z轴垂直屏幕。这些限制给我们的绘图带来了很多不便。 我们生活在一个三维的世界–如果要观察一个物体,我们可以: 1、从不同的位置去观察它。(视图变换) 2、移动或者旋转它,当然了,如果它只是计算机里面的物体,我们还可以放大或缩小它。(模型变换) 3、如果把物体画下来,我们可以选择:是否需要一种”近大远小”的透视效果。另外,我们可能只希望看到物体的一部分,而不是全部(剪裁)。(投影变换) 4、我们可能希望把整个看到的图形画下来,但它只占据纸张的一部分,而不是全部。(视口变换) 这些,都可以在OpenGL中实现。 OpenGL变换实际上是通过矩阵乘法来实现。无论是移动、旋转还是缩放大小,都是通过在当前矩阵的基础上乘以一个新的矩阵来达到目的。关于矩阵的知识,这里不详细介绍,有兴趣的朋友可以看看线性代数(大学生的话多半应该学过的)。 OpenGL可以在最底层直接操作矩阵,不过作为初学,这样做的意义并不大。这里就不做介绍了。
1、模型变换和视图变换 从”相对移动”的观点来看,改变观察点的位置与方向和改变物体本身的位置与方向具有等效性。在OpenGL中,实现这两种功能甚至使用的是同样的函数。 由于模型和视图的变换都通过矩阵运算来实现,在进行变换前,应先设置当前操作的矩阵为”模型视图矩阵”。设置的方法是以GL_MODELVIEW为参数调用glMatrixMode函数,像这样: glMatrixMode(GL_MODELVIEW); 通常,我们需要在进行变换前把当前矩阵设置为单位矩阵。这也只需要一行代码: glLoadIdentity(); 然后,就可以进行模型变换和视图变换了。进行模型和视图变换,主要涉及到三个函数: glTranslate*,把当前矩阵和一个表示移动物体的矩阵相乘。三个参数分别表示了在三个坐标上的位移值。 glRotate*,把当前矩阵和一个表示旋转物体的矩阵相乘。物体将绕着(0,0,0)到(x,y,z)的直线以逆时针旋转,参数angle表示旋转的角度。 glScale*,把当前矩阵和一个表示缩放物体的矩阵相乘。x,y,z分别表示在该方向上的缩放比例。 注意我都是说”与XX相乘”,而不是直接说”这个函数就是旋转”或者”这个函数就是移动”,这是有原因的,马上就会讲到。 假设当前矩阵为单位矩阵,然后先乘以一个表示旋转的矩阵R,再乘以一个表示移动的矩阵T,最后得到的矩阵再乘上每一个顶点的坐标矩阵v。所以,经过变换得到的顶点坐标就是((RT)v)。由于矩阵乘法的结合率,((RT)v) = (R(Tv)),换句话说,实际上是先进行移动,然后进行旋转。即:实际变换的顺序与代码中写的顺序是相反的。由于”先移动后旋转”和”先旋转后移动”得到的结果很可能不同,初学的时候需要特别注意这一点。 OpenGL之所以这样设计,是为了得到更高的效率。但在绘制复杂的三维图形时,如果每次都去考虑如何把变换倒过来,也是很痛苦的事情。这里介绍另一种思路,可以让代码看起来更自然(写出的代码其实完全一样,只是考虑问题时用的方法不同了)。 让我们想象,坐标并不是固定不变的。旋转的时候,坐标系统随着物体旋转。移动的时候,坐标系统随着物体移动。如此一来,就不需要考虑代码的顺序反转的问题了。 以上都是针对改变物体的位置和方向来介绍的。如果要改变观察点的位置,除了配合使用glRotate*和glTranslate*函数以外,还可以使用这个函数:gluLookAt。它的参数比较多,前三个参数表示了观察点的位置,中间三个参数表示了观察目标的位置,最后三个参数代表从(0,0,0)到 (x,y,z)的直线,它表示了观察者认为的”上”方向。
2、投影变换 投影变换就是定义一个可视空间,可视空间以外的物体不会被绘制到屏幕上。(注意,从现在起,坐标可以不再是-1.0到1.0了!) OpenGL支持两种类型的投影变换,即透视投影和正投影。投影也是使用矩阵来实现的。如果需要操作投影矩阵,需要以GL_PROJECTION为参数调用glMatrixMode函数。 glMatrixMode(GL_PROJECTION); 通常,我们需要在进行变换前把当前矩阵设置为单位矩阵。 glLoadIdentity(); 透视投影所产生的结果类似于照片,有近大远小的效果,比如在火车头内向前照一个铁轨的照片,两条铁轨似乎在远处相交了。 使用glFrustum函数可以将当前的可视空间设置为透视投影空间。其参数的意义如下图:
声明:该图片来自www.opengl.org,该图片是《OpenGL编程指南》一书的附图,由于该书的旧版(第一版,1994年)已经流传于网络,我希望没有触及到版权问题。 也可以使用更常用的gluPerspective函数。其参数的意义如下图:
声明:该图片来自www.opengl.org,该图片是《OpenGL编程指南》一书的附图,由于该书的旧版(第一版,1994年)已经流传于网络,我希望没有触及到版权问题。 正投影相当于在无限远处观察得到的结果,它只是一种理想状态。但对于计算机来说,使用正投影有可能获得更好的运行速度。 使用glOrtho函数可以将当前的可视空间设置为正投影空间。其参数的意义如下图: 声明:该图片来自www.opengl.org,该图片是《OpenGL编程指南》一书的附图,由于该书的旧版(第一版,1994年)已经流传于网络,我希望没有触及到版权问题。
如果绘制的图形空间本身就是二维的,可以使用gluOrtho2D。他的使用类似于glOrgho。
3、视口变换 当一切工作已经就绪,只需要把像素绘制到屏幕上了。这时候还剩最后一个问题:应该把像素绘制到窗口的哪个区域呢?通常情况下,默认是完整的填充整个窗口,但我们完全可以只填充一半。(即:把整个图象填充到一半的窗口内) 声明:该图片来自www.opengl.org,该图片是《OpenGL编程指南》一书的附图,由于该书的旧版(第一版,1994年)已经流传于网络,我希望没有触及到版权问题。
使用glViewport来定义视口。其中前两个参数定义了视口的左下脚(0,0表示最左下方),后两个参数分别是宽度和高度。 [...]

News, OpenGL, Tutorial

OpenGL入门学习——第四课

现在放出第四课的内容。 先回顾一下我们都学习了些什么: 第一课,编写第一个OpenGL程序 第二课,绘制几何图形 第三课,绘制几何图形的一些细节问题 第四课,颜色的选择 –→ 本次课程的内容 本次学习的是颜色的选择。终于要走出黑白的世界了~~ OpenGL支持两种颜色模式:一种是RGBA,一种是颜色索引模式。 无论哪种颜色模式,计算机都必须为每一个像素保存一些数据。不同的是,RGBA模式中,数据直接就代表了颜色;而颜色索引模式中,数据代表的是一个索引,要得到真正的颜色,还必须去查索引表。 1. RGBA颜色 RGBA模式中,每一个像素会保存以下数据:R值(红色分量)、G值(绿色分量)、B值(蓝色分量)和A值(alpha分量)。其中红、绿、蓝三种颜色相组合,就可以得到我们所需要的各种颜色,而alpha不直接影响颜色,它将留待以后介绍。 在RGBA模式下选择颜色是十分简单的事情,只需要一个函数就可以搞定。 glColor*系列函数可以用于设置颜色,其中三个参数的版本可以指定R、G、B的值,而A值采用默认;四个参数的版本可以分别指定R、G、B、A的值。例如: void glColor3f(GLfloat red, GLfloat green, GLfloat blue); void glColor4f(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha); (还记得吗?3f表示有三个浮点参数~请看第二课中关于glVertex*函数的叙述。) 将浮点数作为参数,其中0.0表示不使用该种颜色,而1.0表示将该种颜色用到最多。例如: glColor3f(1.0f, 0.0f, 0.0f); 表示不使用绿、蓝色,而将红色使用最多,于是得到最纯净的红色。 glColor3f(0.0f, 1.0f, 1.0f); 表示使用绿、蓝色到最多,而不使用红色。混合的效果就是浅蓝色。 glColor3f(0.5f, 0.5f, 0.5f); 表示各种颜色使用一半,效果为灰色。 注意:浮点数可以精确到小数点后若干位,这并不表示计算机就可以显示如此多种颜色。实际上,计算机可以显示的颜色种数将由硬件决定。如果OpenGL找不到精确的颜色,会进行类似”四舍五入”的处理。 大家可以通过改变下面代码中glColor3f的参数值,绘制不同颜色的矩形。 void myDisplay(void) { glClear(GL_COLOR_BUFFER_BIT); glColor3f(0.0f, 1.0f, 1.0f); [...]

News, OpenGL, Tutorial

OpenGL入门学习——第三课

好了,现在开始放出第三课的内容。 先回顾一下我们都学习了些什么: 第一课,编写第一个OpenGL程序 第二课,绘制几何图形 第三课,绘制几何图形的一些细节问题 –→ 本次课程的内容 在第二课中,我们学习了如何绘制几何图形,但大家如果多写几个程序,就会发现其实还是有些郁闷之处。例如:点太小,难以看清楚;直线也太细,不舒服;或者想画虚线,但不知道方法只能用许多短直线,甚至用点组合而成。 这些问题将在本课中被解决。 下面就点、直线、多边形分别讨论。
1、关于点 点的大小默认为1个像素,但也可以改变之。改变的命令为glPointSize,其函数原型如下: void glPointSize(GLfloat size); size必须大于0.0f,默认值为1.0f,单位为”像素”。 注意:对于具体的OpenGL实现,点的大小都有个限度的,如果设置的size超过最大值,则设置可能会有问题。 例子: void myDisplay(void) { glClear(GL_COLOR_BUFFER_BIT); glPointSize(5.0f); glBegin(GL_POINTS); glVertex2f(0.0f, 0.0f); glVertex2f(0.5f, 0.5f); glEnd(); glFlush(); } 2、关于直线 (1)直线可以指定宽度: void glLineWidth(GLfloat width); 其用法跟glPointSize类似。 (2)画虚线。 首先,使用glEnable(GL_LINE_STIPPLE);来启动虚线模式(使用glDisable(GL_LINE_STIPPLE)可以关闭之)。 然后,使用glLineStipple来设置虚线的样式。 void glLineStipple(GLint factor, GLushort pattern); pattern是由1和0组成的长度为16的序列,从最低位开始看,如果为1,则直线上接下来应该画的factor个点将被画为实的;如果为0,则直线上接下来应该画的factor个点将被画为虚的。 以下是一些例子: 声明:该图片来自www.opengl.org,该图片是《OpenGL编程指南》一书的附图,由于该书的旧版(第一版,1994年)已经流传于网络,我希望没有触及到版权问题。 示例代码: void myDisplay(void) { glClear(GL_COLOR_BUFFER_BIT); glEnable(GL_LINE_STIPPLE); glLineStipple(2, [...]

News, OpenGL, Tutorial