opengl入门3

来源:互联网 发布:手机淘宝网店认证 编辑:程序博客网 时间:2024/05/21 07:57

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函数可以将当前的可视空间设置为透视投影空间。其参数的意义如下图:
http://blog.programfan.com/upfile/200610/20061007151547.gif
声明:该图片来自www.opengl.org,该图片是《OpenGL编程指南》一书的附图,由于该书的旧版(第一版,1994年)已经流传于网络,我希望没有触及到版权问题。
也可以使用更常用的gluPerspective函数。其参数的意义如下图:
http://blog.programfan.com/upfile/200610/2006100715161.gif
声明:该图片来自www.opengl.org,该图片是《OpenGL编程指南》一书的附图,由于该书的旧版(第一版,1994年)已经流传于网络,我希望没有触及到版权问题。

正投影相当于在无限远处观察得到的结果,它只是一种理想状态。但对于计算机来说,使用正投影有可能获得更好的运行速度。
使用glOrtho函数可以将当前的可视空间设置为正投影空间。其参数的意义如下图:
http://blog.programfan.com/upfile/200610/20061007151619.gif
声明:该图片来自www.opengl.org,该图片是《OpenGL编程指南》一书的附图,由于该书的旧版(第一版,1994年)已经流传于网络,我希望没有触及到版权问题。

如果绘制的图形空间本身就是二维的,可以使用gluOrtho2D。他的使用类似于glOrgho。


3、视口变换
当一切工作已经就绪,只需要把像素绘制到屏幕上了。这时候还剩最后一个问题:应该把像素绘制到窗口的哪个区域呢?通常情况下,默认是完整的填充整个窗口,但我们完全可以只填充一半。(即:把整个图象填充到一半的窗口内)
http://blog.programfan.com/upfile/200610/20061007151639.gif
声明:该图片来自www.opengl.org,该图片是《OpenGL编程指南》一书的附图,由于该书的旧版(第一版,1994年)已经流传于网络,我希望没有触及到版权问题。

使用glViewport来定义视口。其中前两个参数定义了视口的左下脚(0,0表示最左下方),后两个参数分别是宽度和高度。

4、操作矩阵堆栈
介于是入门教程,先简单介绍一下堆栈。你可以把堆栈想象成一叠盘子。开始的时候一个盘子也没有,你可以一个一个往上放,也可以一个一个取下来。每次取下的,都是最后一次被放上去的盘子。通常,在计算机实现堆栈时,堆栈的容量是有限的,如果盘子过多,就会出错。当然,如果没有盘子了,再要求取一个盘子,也会出错。
我们在进行矩阵操作时,有可能需要先保存某个矩阵,过一段时间再恢复它。当我们需要保存时,调用glPushMatrix函数,它相当于把矩阵(相当于盘子)放到堆栈上。当需要恢复最近一次的保存时,调用glPopMatrix函数,它相当于把矩阵从堆栈上取下。OpenGL规定堆栈的容量至少可以容纳32个矩阵,某些OpenGL实现中,堆栈的容量实际上超过了32个。因此不必过于担心矩阵的容量问题。
通常,用这种先保存后恢复的措施,比先变换再逆变换要更方便,更快速。
注意:模型视图矩阵和投影矩阵都有相应的堆栈。使用glMatrixMode来指定当前操作的究竟是模型视图矩阵还是投影矩阵。

5、综合举例
好了,视图变换的入门知识差不多就讲完了。但我们不能就这样结束。因为本次课程的内容实在过于枯燥,如果分别举例,可能效果不佳。我只好综合的讲一个例子,算是给大家一个参考。至于实际的掌握,还要靠大家自己花功夫。闲话少说,现在进入正题。

我们要制作的是一个三维场景,包括了太阳、地球和月亮。假定一年有12个月,每个月30天。每年,地球绕着太阳转一圈。每个月,月亮围着地球转一圈。即一年有360天。现在给出日期的编号(0~359),要求绘制出太阳、地球、月亮的相对位置示意图。(这是为了编程方便才这样设计的。如果需要制作更现实的情况,那也只是一些数值处理而已,与OpenGL关系不大)
首先,让我们认定这三个天体都是球形,且他们的运动轨迹处于同一水平面,建立以下坐标系:太阳的中心为原点,天体轨迹所在的平面表示了X轴与Y轴决定的平面,且每年第一天,地球在X轴正方向上,月亮在地球的正X轴方向。
下一步是确立可视空间。注意:太阳的半径要比太阳到地球的距离短得多。如果我们直接使用天文观测得到的长度比例,则当整个窗口表示地球轨道大小时,太阳的大小将被忽略。因此,我们只能成倍的放大几个天体的半径,以适应我们观察的需要。(百度一下,得到太阳、地球、月亮的大致半径分别是:696000km, 6378km,1738km。地球到太阳的距离约为1.5亿km=150000000km,月亮到地球的距离约为380000km。)
让我们假想一些数据,将三个天体的半径分别“修改”为:69600000(放大100倍),15945000(放大2500倍),4345000(放大5000倍)。将地球到月亮的距离“修改”为38000000(放大100倍)。地球到太阳的距离保持不变。
为了让地球和月亮在离我们很近时,我们仍然不需要变换观察点和观察方向就可以观察它们,我们把观察点放在这个位置:(0, -200000000, 0) ——因为地球轨道半径为150000000,咱们就凑个整,取-200000000就可以了。观察目标设置为原点(即太阳中心),选择Z轴正方向作为 “上”方。当然我们还可以把观察点往“上”方移动一些,得到(0, -200000000, 200000000),这样可以得到45度角的俯视效果。
为了得到透视效果,我们使用gluPerspective来设置可视空间。假定可视角为60度(如果调试时发现该角度不合适,可修改之。我在最后选择的数值是75。),高宽比为1.0。最近可视距离为1.0,最远可视距离为200000000*2=400000000。即:gluPerspective (60, 1, 1, 400000000);


5、综合举例
好了,视图变换的入门知识差不多就讲完了。但我们不能就这样结束。因为本次课程的内容实在过于枯燥,如果分别举例,可能效果不佳。我只好综合的讲一个例子,算是给大家一个参考。至于实际的掌握,还要靠大家自己花功夫。闲话少说,现在进入正题。

我们要制作的是一个三维场景,包括了太阳、地球和月亮。假定一年有12个月,每个月30天。每年,地球绕着太阳转一圈。每个月,月亮围着地球转一圈。即一年有360天。现在给出日期的编号(0~359),要求绘制出太阳、地球、月亮的相对位置示意图。(这是为了编程方便才这样设计的。如果需要制作更现实的情况,那也只是一些数值处理而已,与OpenGL关系不大)
首先,让我们认定这三个天体都是球形,且他们的运动轨迹处于同一水平面,建立以下坐标系:太阳的中心为原点,天体轨迹所在的平面表示了X轴与Y轴决定的平面,且每年第一天,地球在X轴正方向上,月亮在地球的正X轴方向。
下一步是确立可视空间。注意:太阳的半径要比太阳到地球的距离短得多。如果我们直接使用天文观测得到的长度比例,则当整个窗口表示地球轨道大小时,太阳的大小将被忽略。因此,我们只能成倍的放大几个天体的半径,以适应我们观察的需要。(百度一下,得到太阳、地球、月亮的大致半径分别是:696000km, 6378km,1738km。地球到太阳的距离约为1.5亿km=150000000km,月亮到地球的距离约为380000km。)
让我们假想一些数据,将三个天体的半径分别“修改”为:69600000(放大100倍),15945000(放大2500倍),4345000(放大2500倍)。将地球到月亮的距离“修改”为38000000(放大100倍)。地球到太阳的距离保持不变。
为了让地球和月亮在离我们很近时,我们仍然不需要变换观察点和观察方向就可以观察它们,我们把观察点放在这个位置:(0, -200000000, 0) ——因为地球轨道半径为150000000,咱们就凑个整,取-200000000就可以了。观察目标设置为原点(即太阳中心),选择Z轴正方向作为 “上”方。当然我们还可以把观察点往“上”方移动一些,得到(0, -200000000, 200000000),这样可以得到45度角的俯视效果。
为了得到透视效果,我们使用gluPerspective来设置可视空间。假定可视角为60度(如果调试时发现该角度不合适,可修改之。我在最后选择的数值是75。),高宽比为1.0。最近可视距离为1.0,最远可视距离为200000000*2=400000000。即:gluPerspective (60, 1, 1, 400000000);


现在我们来看看如何绘制这三个天体。
为了简单起见,我们把三个天体都想象成规则的球体。而我们所使用的glut实用工具中,正好就有一个绘制球体的现成函数:glutSolidSphere,这个函数在“原点”绘制出一个球体。由于坐标是可以通过glTranslate*和glRotate*两个函数进行随意变换的,所以我们就可以在任意位置绘制球体了。函数有三个参数:第一个参数表示球体的半径,后两个参数代表了“面”的数目,简单点说就是球体的精确程度,数值越大越精确,当然代价就是速度越缓慢。这里我们只是简单的设置后两个参数为20。
太阳在坐标原点,所以不需要经过任何变换,直接绘制就可以了。
地球则要复杂一点,需要变换坐标。由于今年已经经过的天数已知为day,则地球转过的角度为day/一年的天数*360度。前面已经假定每年都是360天,因此地球转过的角度恰好为day。所以可以通过下面的代码来解决:
glRotatef(day, 0, 0, -1);
/* 注意地球公转是“自西向东”的,因此是饶着Z轴负方向进行逆时针旋转 */
glTranslatef(地球轨道半径, 0, 0);
glutSolidSphere(地球半径, 20, 20);
月亮是最复杂的。因为它不仅要绕地球转,还要随着地球绕太阳转。但如果我们选择地球作为参考,则月亮进行的运动就是一个简单的圆周运动了。如果我们先绘制地球,再绘制月亮,则只需要进行与地球类似的变换:
glRotatef(月亮旋转的角度, 0, 0, -1);
glTranslatef(月亮轨道半径, 0, 0);
glutSolidSphere(月亮半径, 20, 20);
但这个“月亮旋转的角度”,并不能简单的理解为day/一个月的天数30*360度。因为我们在绘制地球时,这个坐标已经是旋转过的。现在的旋转是在以前的基础上进行旋转,因此还需要处理这个“差值”。我们可以写成:day/30*360 - day,即减去原来已经转过的角度。这只是一种简单的处理,当然也可以在绘制地球前用glPushMatrix保存矩阵,绘制地球后用glPopMatrix恢复矩阵。再设计一个跟地球位置无关的月亮位置公式,来绘制月亮。通常后一种方法比前一种要好,因为浮点的运算是不精确的,即是说我们计算地球本身的位置就是不精确的。拿这个不精确的数去计算月亮的位置,会导致 “不精确”的成分累积,过多的“不精确”会造成错误。我们这个小程序没有去考虑这个,但并不是说这个问题不重要。
还有一个需要注意的细节: OpenGL把三维坐标中的物体绘制到二维屏幕,绘制的顺序是按照代码的顺序来进行的。因此后绘制的物体会遮住先绘制的物体,即使后绘制的物体在先绘制的物体的“后面”也是如此。使用深度测试可以解决这一问题。使用的方法是:1、以GL_DEPTH_TEST为参数调用glEnable函数,启动深度测试。2、在必要时(通常是每次绘制画面开始时),清空深度缓冲,即:glClear(GL_DEPTH_BUFFER_BIT);其中,glClear (GL_COLOR_BUFFER_BIT)与glClear(GL_DEPTH_BUFFER_BIT)可以合并写为:
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
且后者的运行速度可能比前者快。


到此为止,我们终于可以得到整个“太阳,地球和月亮”系统的完整代码。


Code:
--------------------------------------------------------------------------------
// 太阳、地球和月亮
// 假设每个月都是30天
// 一年12个月,共是360天
static int day = 200; // day的变化:从0到359
void myDisplay(void)
{
     glEnable(GL_DEPTH_TEST);
     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

     glMatrixMode(GL_PROJECTION);
     glLoadIdentity();
     gluPerspective(75, 1, 1, 400000000);
     glMatrixMode(GL_MODELVIEW);
     glLoadIdentity();
     gluLookAt(0, -200000000, 200000000, 0, 0, 0, 0, 0, 1);

     // 绘制红色的“太阳”
     glColor3f(1.0f, 0.0f, 0.0f);
     glutSolidSphere(69600000, 20, 20);
     // 绘制蓝色的“地球”
     glColor3f(0.0f, 0.0f, 1.0f);
     glRotatef(day/360.0*360.0, 0.0f, 0.0f, -1.0f);
     glTranslatef(150000000, 0.0f, 0.0f);
     glutSolidSphere(15945000, 20, 20);
     // 绘制黄色的“月亮”
     glColor3f(1.0f, 1.0f, 0.0f);
     glRotatef(day/30.0*360.0 - day/360.0*360.0, 0.0f, 0.0f, -1.0f);
     glTranslatef(38000000, 0.0f, 0.0f);
     glutSolidSphere(4345000, 20, 20);

     glFlush();
}
--------------------------------------------------------------------------------



试修改day的值,看看画面有何变化。


小结:本课开始,我们正式进入了三维的OpenGL世界。
OpenGL通过矩阵变换来把三维物体转变为二维图象,进而在屏幕上显示出来。为了指定当前操作的是何种矩阵,我们使用了函数glMatrixMode。
我们可以移动、旋转观察点或者移动、旋转物体,使用的函数是glTranslate*和glRotate*。
我们可以缩放物体,使用的函数是glScale*。
我们可以定义可视空间,这个空间可以是“正投影”的(使用glOrtho或gluOrtho2D),也可以是“透视投影”的(使用glFrustum或gluPerspective)。
我们可以定义绘制到窗口的范围,使用的函数是glViewport。
矩阵有自己的“堆栈”,方便进行保存和恢复。这在绘制复杂图形时很有帮助。使用的函数是glPushMatrix和glPopMatrix。

好了,艰苦的一课终于完毕。我知道,本课的内容十分枯燥,就连最后的例子也是。但我也没有更好的办法了,希望大家能坚持过去。不必担心,熟悉本课内容后,以后的一段时间内,都会是比较轻松愉快的了。

=====================    第五课 完    =====================
=====================TO BE CONTINUED=====================



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还提供了一些别的函数,例如“在键盘按下时做某事”等。

到现在,我们已经可以初步开始制作动画了。好的,就拿上次那个“太阳、地球和月亮”的程序开刀,让地球和月亮自己动起来。

Code:

#include <GL/glut.h>

// 太阳、地球和月亮
// 假设每个月都是30天
// 一年12个月,共是360天
static int day = 200; // day的变化:从0到359
void myDisplay(void)
{
     /****************************************************
      这里的内容照搬上一课的,只因为使用了双缓冲,补上最后这句
     *****************************************************/
     glutSwapBuffers();
}

void myIdle(void)
{
     /* 新的函数,在空闲时调用,作用是把日期往后移动一天并重新绘制,达到动画效果 */
     ++day;
     if( day >= 360 )
         day = 0;
     myDisplay();
}

int main(int argc, char *argv[])
{
     glutInit(&argc, argv);
     glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE); // 修改了参数为GLUT_DOUBLE
     glutInitWindowPosition(100, 100);
     glutInitWindowSize(400, 400);
     glutCreateWindow("太阳,地球和月亮");    // 改了窗口标题
     glutDisplayFunc(&myDisplay);
     glutIdleFunc(&myIdle);                // 新加入了这句
     glutMainLoop();
     return 0;
}


3、关于垂直同步
代码是写好了,但相信大家还有疑问。某些朋友可能在运行时发现,虽然CPU几乎都用上了,但运动速度很快,根本看不清楚,另一些朋友在运行时发现CPU使用率很低,根本就没有把空闲时间完全利用起来。但对于上面那段代码来说,这些现象都是合理的。这里就牵涉到关于垂直同步的问题。

大家知道显示器的刷新率是比较有限的,一般为60~120Hz,也就是一秒钟刷新60~120次。但如果叫计算机绘制一个简单的画面,例如只有一个三角形,则一秒钟可以绘制成千上万次。因此,如果最大限度的利用计算机的处理能力,绘制很多幅画面,但显示器的刷新速度却跟不上,这不仅造成性能的浪费,还可能带来一些负面影响(例如,显示器只刷新到一半时,需要绘制的内容却变化了,由于显示器是逐行刷新的,于是显示器上半部分和下半部分实际上是来自两幅画面)。采用垂直同步技术可以解决这一问题。即,只有在显示器刷新时,才把绘制好的图象传输出去供显示。这样一来,计算机就不必去绘制大量的根本就用不到的图象了。如果显示器的刷新率为85Hz,则计算机一秒钟只需要绘制85幅图象就足够,如果场景足够简单,就会造成比较多的CPU空闲。
几乎所有的显卡都支持“垂直同步”这一功能。
垂直同步也有它的问题。如果刷新频率为60Hz,则在绘制比较简单的场景时,绘制一幅图画需要的时间很段,帧速可以恒定在60FPS(即60帧/秒)。如果场景变得复杂,绘制一幅图画的时间超过了1/60秒,则帧速将急剧下降。
如果绘制一幅图画的时间为1/50,则在第一个1/60秒时,显示器需要刷新了,但由于新的图画没有画好,所以只能显示原来的图画,等到下一个1/60秒时才显示新的图画。于是显示一幅图画实际上用了1/30秒,帧速为30FPS。(如果不采用垂直同步,则帧速应该是50FPS)
如果绘制一幅图画的时间更长,则下降的趋势就是阶梯状的:60FPS,30FPS,20FPS,……(60/1,60/2,60/3,……)
如果每一幅图画的复杂程度是不一致的,且绘制它们需要的时间都在1/60上下。则在1/60时间内画完时,帧速为60FPS,在1/60时间未完成时,帧速为30FPS,这就造成了帧速的跳动。这是很麻烦的事情,需要避免它——要么想办法简化每一画面的绘制时间,要么都延迟一小段时间,以作到统一。

回过头来看前面的问题。如果使用了大量的CPU而且速度很快无法看清,则打开垂直同步可以解决该问题。当然如果你认为垂直同步有这样那样的缺点,也可以关闭它。——至于如何打开和关闭,因操作系统而异了。具体步骤请自己搜索之。

当然,也有其它办法可以控制动画的帧速,或者尽量让动画的速度尽量和帧速无关。不过这里面很多内容都是与操作系统比较紧密的,况且它们跟OpenGL关系也不太大。这里就不做介绍了。


4、计算帧速
不知道大家玩过3D Mark这个软件没有,它可以运行各种场景,测出帧速,并且为你的系统给出评分。这里我也介绍一个计算帧速的方法。
根据定义,帧速就是一秒钟内播放的画面数目(FPS)。我们可以先测量绘制两幅画面之间时间t,然后求它的倒数即可。假如t=0.05s,则FPS的值就是1/0.05=20。
理论上是如此了,可是如何得到这个时间呢?通常C语言的time函数精确度一般只到一秒,肯定是不行了。clock函数也就到十毫秒左右,还是有点不够。因为FPS为60和FPS为100的时候,t的值都是十几毫秒。
你知道如何测量一张纸的厚度吗?一个粗略的办法就是:用很多张纸叠在一起测厚度,计算平均值就可以了。我们这里也可以这样办。测量绘制50幅画面(包括垂直同步等因素的等待时间)需要的时间t',由t'=t*50很容易的得到FPS=1/t=50/t'
下面这段代码可以统计该函数自身的调用频率,(原理就像上面说的那样),程序并不复杂,并且这并不属于OpenGL的内容,所以我不打算详细讲述它。

Code:

#include <time.h>
double CalFrequency()
{
     static int count;
     static double save;
     static clock_t last, current;
     double timegap;

     ++count;
     if( count <= 50 )
         return save;
     count = 0;
     last = current;
     current = clock();
     timegap = (current-last)/(double)CLK_TCK;
     save = 50.0/timegap;
     return save;
}


最后,要把计算的帧速显示出来,但我们并没有学习如何使用OpenGL把文字显示到屏幕上。——但不要忘了,在我们的图形窗口背后,还有一个命令行窗口~使用printf函数就可以轻易的输出文字了。
#include <stdio.h>

double FPS = CalFrequency();
printf("FPS = %f\n", FPS);
最后的一步,也被我们解决了——虽然做法不太雅观,没关系,以后我们还会改善它的。


时间过得太久,每次给的程序都只是一小段,一些朋友难免会出问题。
现在,我给出一个比较完整的程序,供大家参考。

Code:

#include <GL/glut.h>
#include <stdio.h>
#include <time.h>

// 太阳、地球和月亮
// 假设每个月都是12天
// 一年12个月,共是360天
static int day = 200; // day的变化:从0到359

double CalFrequency()
{
     static int count;
     static double save;
     static clock_t last, current;
     double timegap;

     ++count;
     if( count <= 50 )
         return save;
     count = 0;
     last = current;
     current = clock();
     timegap = (current-last)/(double)CLK_TCK;
     save = 50.0/timegap;
     return save;
}

void myDisplay(void)
{
     double FPS = CalFrequency();
     printf("FPS = %f\n", FPS);

     glEnable(GL_DEPTH_TEST);
     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

     glMatrixMode(GL_PROJECTION);
     glLoadIdentity();
     gluPerspective(75, 1, 1, 400000000);
     glMatrixMode(GL_MODELVIEW);
     glLoadIdentity();
     gluLookAt(0, -200000000, 200000000, 0, 0, 0, 0, 0, 1);

     // 绘制红色的“太阳”
     glColor3f(1.0f, 0.0f, 0.0f);
     glutSolidSphere(69600000, 20, 20);
     // 绘制蓝色的“地球”
     glColor3f(0.0f, 0.0f, 1.0f);
     glRotatef(day/360.0*360.0, 0.0f, 0.0f, -1.0f);
     glTranslatef(150000000, 0.0f, 0.0f);
     glutSolidSphere(15945000, 20, 20);
     // 绘制黄色的“月亮”
     glColor3f(1.0f, 1.0f, 0.0f);
     glRotatef(day/30.0*360.0 - day/360.0*360.0, 0.0f, 0.0f, -1.0f);
     glTranslatef(38000000, 0.0f, 0.0f);
     glutSolidSphere(4345000, 20, 20);

     glFlush();
     glutSwapBuffers();
}

void myIdle(void)
{
     ++day;
     if( day >= 360 )
         day = 0;
     myDisplay();
}

int main(int argc, char *argv[])
{
     glutInit(&argc, argv);
     glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE);
     glutInitWindowPosition(100, 100);
     glutInitWindowSize(400, 400);
     glutCreateWindow("太阳,地球和月亮");
     glutDisplayFunc(&myDisplay);
     glutIdleFunc(&myIdle);
     glutMainLoop();
     return 0;
}


小结:
OpenGL动画和传统意义上的动画相似,都是把画面一幅一幅的呈现在观众面前。一旦画面变换的速度快了,观众就会认为画面是连续的。
双缓冲技术是一种在计算机图形中普遍采用的技术,绝大多数OpenGL实现都支持双缓冲技术。
通常都是利用CPU空闲的时候绘制动画,但也可以有其它的选择。
介绍了垂直同步的相关知识。
介绍了一种简单的计算帧速(FPS)的方法。
最后,我们列出了一份完整的天体动画程序清单。

=====================    第六课 完    =====================
=====================TO BE CONTINUED=====================




OpenGL入门学习[七]


今天要讲的是OpenGL光照的基本知识。虽然内容显得有点多,但条理还算比较清晰,理解起来应该没有困难。即使对于一些内容没有记住,问题也不大——光照部分是一个比较独立的内容,它的学习与其它方面的学习可以分开,不像视图变换那样,影响到许多方面。课程的最后给出了一个有关光照效果的动画演示程序,我想大家会喜欢的。
从生理学的角度上讲,眼睛之所以看见各种物体,是因为光线直接或间接的从它们那里到达了眼睛。人类对于光线强弱的变化的反应,比对于颜色变化的反应来得灵敏。因此对于人类而言,光线很大程度上表现了物体的立体感。
请看图1,图中绘制了两个大小相同的白色球体。其中右边的一个是没有使用任何光照效果的,它看起来就像是一个二维的圆盘,没有立体的感觉。左边的一个是使用了简单的光照效果的,我们通过光照的层次,很容易的认为它是一个三维的物体。
http://blog.programfan.com/upfile/200702/2007022315149.jpg
图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度,即不使用聚光灯,向全周围发射。
http://blog.programfan.com/upfile/200702/20070223151415.gif
图2

(4)GL_CONSTANT_ATTENUATION、GL_LINEAR_ATTENUATION、GL_QUADRATIC_ATTENUATION属性。这三个属性表示了光源所发出的光线的直线传播特性(这些属性只对位置性光源有效)。现实生活中,光线的强度随着距离的增加而减弱,OpenGL把这个减弱的趋势抽象成函数:
衰减因子 = 1 / (k1 + k2 * d + k3 * k3 * d)
其中d表示距离,光线的初始强度乘以衰减因子,就得到对应距离的光线强度。k1, k2, k3分别就是GL_CONSTANT_ATTENUATION, GL_LINEAR_ATTENUATION, GL_QUADRATIC_ATTENUATION。通过设置这三个常数,就可以控制光线在传播过程中的减弱趋势。

属性还真是不少。当然了,如果是使用方向性光源,(3)(4)这两类属性就不会用到了,问题就变得简单明了。
四、控制材质
材质与光源相似,也需要设置众多的属性。不同的是,光源是通过glLight*函数来设置的,而材质则是通过glMaterial*函数来设置的。
glMaterial*函数有三个参数。第一个参数表示指定哪一面的属性。可以是GL_FRONT、GL_BACK或者GL_FRONT_AND_BACK。分别表示设置“正面”“背面”的材质,或者两面同时设置。(关于“正面”“背面”的内容需要参看前些课程的内容)第二、第三个参数与glLight*函数的第二、三个参数作用类似。下面分别说明glMaterial*函数可以指定的材质属性。
(1)GL_AMBIENT、GL_DIFFUSE、GL_SPECULAR属性。这三个属性与光源的三个对应属性类似,每一属性都由四个值组成。GL_AMBIENT表示各种光线照射到该材质上,经过很多次反射后最终遗留在环境中的光线强度(颜色)。GL_DIFFUSE表示光线照射到该材质上,经过漫反射后形成的光线强度(颜色)。GL_SPECULAR表示光线照射到该材质上,经过镜面反射后形成的光线强度(颜色)。通常,GL_AMBIENT和GL_DIFFUSE都取相同的值,可以达到比较真实的效果。使用GL_AMBIENT_AND_DIFFUSE可以同时设置GL_AMBIENT和GL_DIFFUSE属性。
(2)GL_SHININESS属性。该属性只有一个值,称为“镜面指数”,取值范围是0到128。该值越小,表示材质越粗糙,点光源发射的光线照射到上面,也可以产生较大的亮点。该值越大,表示材质越类似于镜面,光源照射到上面后,产生较小的亮点。
(3)GL_EMISSION属性。该属性由四个值组成,表示一种颜色。OpenGL认为该材质本身就微微的向外发射光线,以至于眼睛感觉到它有这样的颜色,但这光线又比较微弱,以至于不会影响到其它物体的颜色。
(4)GL_COLOR_INDEXES属性。该属性仅在颜色索引模式下使用,由于颜色索引模式下的光照比RGBA模式要复杂,并且使用范围较小,这里不做讨论。
五、选择光照模型
这里所说的“光照模型”是OpenGL的术语,它相当于我们在前面提到的“光照环境”。在OpenGL中,光照模型包括四个部分的内容:全局环境光线(即那些充分散射,无法分清究竟来自哪个光源的光线)的强度、观察点位置是在较近位置还是在无限远处、物体正面与背面是否分别计算光照、镜面颜色(即GL_SPECULAR属性所指定的颜色)的计算是否从其它光照计算中分离出来,并在纹理操作以后在进行应用。
以上四方面的内容都通过同一个函数glLightModel*来进行设置。该函数有两个参数,第一个表示要设置的项目,第二个参数表示要设置成的值。
GL_LIGHT_MODEL_AMBIENT表示全局环境光线强度,由四个值组成。
GL_LIGHT_MODEL_LOCAL_VIEWER表示是否在近处观看,若是则设置为GL_TRUE,否则(即在无限远处观看)设置为GL_FALSE。
GL_LIGHT_MODEL_TWO_SIDE表示是否执行双面光照计算。如果设置为GL_TRUE,则OpenGL不仅将根据法线向量计算正面的光照,也会将法线向量反转并计算背面的光照。
GL_LIGHT_MODEL_COLOR_CONTROL表示颜色计算方式。如果设置为GL_SINGLE_COLOR,表示按通常顺序操作,先计算光照,再计算纹理。如果设置为GL_SEPARATE_SPECULAR_COLOR,表示将GL_SPECULAR属性分离出来,先计算光照的其它部分,待纹理操作完成后再计算GL_SPECULAR。后者通常可以使画面效果更为逼真(当然,如果本身就没有执行任何纹理操作,这样的分离就没有任何意义)。

六、最后的准备
到现在可以说是完事俱备了。不过,OpenGL默认是关闭光照处理的。要打开光照处理功能,使用下面的语句:
glEnable(GL_LIGHTING);
要关闭光照处理功能,使用glDisable(GL_LIGHTING);即可。
七、示例程序
到现在,我们已经可以编写简单的使用光照的OpenGL程序了。
我们仍然以太阳、地球作为例子(这次就不考虑月亮了^-^),把太阳作为光源,模拟地球围绕太阳转动时光照的变化。于是,需要设置一个光源——太阳,设置两种材质——太阳的材质和地球的材质。把太阳光线设置为白色,位置在画面正中。把太阳的材质设置为微微散发出红色的光芒,把地球的材质设置为微微散发出暗淡的蓝色光芒,并且反射蓝色的光芒,镜面指数设置成一个比较小的值。简单起见,不再考虑太阳和地球的大小关系,用同样大小的球体来代替之。
关于法线向量。球体表面任何一点的法线向量,就是球心到该点的向量。如果使用glutSolidSphere函数来绘制球体,则该函数会自动的指定这些法线向量,不必再手工指出。如果是自己指定若干的顶点来绘制一个球体,则需要自己指定法线响亮。
由于我们使用的太阳是一个位置性光源,在设置它的位置时,需要利用到矩阵变换。因此,在设置光源的位置以前,需要先设置好各种矩阵。利用gluPerspective函数来创建具有透视效果的视图。我们也将利用前面课程所学习的动画知识,让整个画面动起来。

下面给出具体的代码:
#include <gl/glut.h>

#define WIDTH 400
#define HEIGHT 400

static GLfloat angle = 0.0f;

void myDisplay(void)
{
     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

     // 创建透视效果视图
     glMatrixMode(GL_PROJECTION);
     glLoadIdentity();
     gluPerspective(90.0f, 1.0f, 1.0f, 20.0f);
     glMatrixMode(GL_MODELVIEW);
     glLoadIdentity();
     gluLookAt(0.0, 5.0, -10.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);

     // 定义太阳光源,它是一种白色的光源
     {
     GLfloat sun_light_position[] = {0.0f, 0.0f, 0.0f, 1.0f};
     GLfloat sun_light_ambient[]   = {0.0f, 0.0f, 0.0f, 1.0f};
     GLfloat sun_light_diffuse[]   = {1.0f, 1.0f, 1.0f, 1.0f};
     GLfloat sun_light_specular[] = {1.0f, 1.0f, 1.0f, 1.0f};

     glLightfv(GL_LIGHT0, GL_POSITION, sun_light_position);
     glLightfv(GL_LIGHT0, GL_AMBIENT,   sun_light_ambient);
     glLightfv(GL_LIGHT0, GL_DIFFUSE,   sun_light_diffuse);
     glLightfv(GL_LIGHT0, GL_SPECULAR, sun_light_specular);

     glEnable(GL_LIGHT0);
     glEnable(GL_LIGHTING);
     glEnable(GL_DEPTH_TEST);
     }

     // 定义太阳的材质并绘制太阳
     {
         GLfloat sun_mat_ambient[]   = {0.0f, 0.0f, 0.0f, 1.0f};
         GLfloat sun_mat_diffuse[]   = {0.0f, 0.0f, 0.0f, 1.0f};
         GLfloat sun_mat_specular[] = {0.0f, 0.0f, 0.0f, 1.0f};
         GLfloat sun_mat_emission[] = {0.5f, 0.0f, 0.0f, 1.0f};
         GLfloat sun_mat_shininess   = 0.0f;

         glMaterialfv(GL_FRONT, GL_AMBIENT,    sun_mat_ambient);
         glMaterialfv(GL_FRONT, GL_DIFFUSE,    sun_mat_diffuse);
         glMaterialfv(GL_FRONT, GL_SPECULAR,   sun_mat_specular);
         glMaterialfv(GL_FRONT, GL_EMISSION,   sun_mat_emission);
         glMaterialf (GL_FRONT, GL_SHININESS, sun_mat_shininess);

         glutSolidSphere(2.0, 40, 32);
     }

     // 定义地球的材质并绘制地球
     {
         GLfloat earth_mat_ambient[]   = {0.0f, 0.0f, 0.5f, 1.0f};
         GLfloat earth_mat_diffuse[]   = {0.0f, 0.0f, 0.5f, 1.0f};
         GLfloat earth_mat_specular[] = {0.0f, 0.0f, 1.0f, 1.0f};
         GLfloat earth_mat_emission[] = {0.0f, 0.0f, 0.0f, 1.0f};
         GLfloat earth_mat_shininess   = 30.0f;

         glMaterialfv(GL_FRONT, GL_AMBIENT,    earth_mat_ambient);
         glMaterialfv(GL_FRONT, GL_DIFFUSE,    earth_mat_diffuse);
         glMaterialfv(GL_FRONT, GL_SPECULAR,   earth_mat_specular);
         glMaterialfv(GL_FRONT, GL_EMISSION,   earth_mat_emission);
         glMaterialf (GL_FRONT, GL_SHININESS, earth_mat_shininess);

         glRotatef(angle, 0.0f, -1.0f, 0.0f);
         glTranslatef(5.0f, 0.0f, 0.0f);
         glutSolidSphere(2.0, 40, 32);
     }

     glutSwapBuffers();
}
void myIdle(void)
{
     angle += 1.0f;
     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光照的基本知识。OpenGL把光照分解为光源、材质、光照模式三个部分,根据这三个部分的各种信息,以及物体表面的法线向量,可以计算得到最终的光照效果。
光源、材质和光照模式都有各自的属性,尽管属性种类繁多,但这些属性都只用很少的几个函数来设置。使用glLight*函数可设置光源的属性,使用glMaterial*函数可设置材质的属性,使用glLightModel*函数可设置光照模式。
GL_AMBIENT、GL_DIFFUSE、GL_SPECULAR这三种属性是光源和材质所共有的,如果某光源发出的光线照射到某材质的表面,则最终的漫反射强度由两个GL_DIFFUSE属性共同决定,最终的镜面反射强度由两个GL_SPECULAR属性共同决定。
可以使用多个光源来实现各种逼真的效果,然而,光源数量的增加将造成程序运行速度的明显下降。
在使用OpenGL光照过程中,属性的种类和数量都非常繁多,通常,需要很多的经验才可以熟练的设置各种属性,从而形成逼真的光照效果。(各位也看到了,其实这个课程的示例程序中,属性设置也不怎么好)。然而,设置这些属性的艺术性远远超过了技术性,往往是一些美术制作人员设置好各种属性(并保存为文件),然后由程序员编写的程序去执行绘制工作。因此,即使目前无法熟练运用各种属性,也不必过于担心。如果条件允许,可以玩玩类似3DS MAX之类的软件,对理解光照、熟悉各种属性设置会有一些帮助。
在课程的最后,我们给出了一个样例程序,演示了太阳和地球模型中的光照效果。






0 0
原创粉丝点击