假设某动画一共有n幅画面,则它的工作步骤就是:
显示第1幅画面,然后等待一小段时间,直到下一个1/24秒
显示第2幅画面,然后等待一小段时间,直到下一个1/24秒
……
显示第n幅画面,然后等待一小段时间,直到下一个1/24秒
结束
如果用C语言伪代码来描述这一过程,就是:

for(i=; 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=; 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>

// 太阳、地球和月亮
// 假设每个月都是30天
// 一年12个月,共是360天
static int day = ; // day的变化:从0到359
void myDisplay(void)
{
glEnable(GL_DEPTH_TEST);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(, , , );
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
gluLookAt(, -, , , , , , , ); // 绘制红色的“太阳”
glColor3f(1.0f, 0.0f, 0.0f);
glutSolidSphere(, , );
// 绘制蓝色的“地球”
glColor3f(0.0f, 0.0f, 1.0f);
glRotatef(day/360.0*360.0, 0.0f, 0.0f, -1.0f);
glTranslatef(, 0.0f, 0.0f);
glutSolidSphere(, , );
// 绘制黄色的“月亮”
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(, 0.0f, 0.0f);
glutSolidSphere(, , ); glFlush(); /****************************************************
这里的内容照搬上一课的,只因为使用了双缓冲,补上最后这句
*****************************************************/
glutSwapBuffers();
} void myIdle(void)
{
/* 新的函数,在空闲时调用,作用是把日期往后移动一天并重新绘制,达到动画效果 */
++day;
if( day >= )
day = ;
myDisplay();
} int main(int argc, char *argv[])
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE); // 修改了参数为GLUT_DOUBLE
glutInitWindowPosition(, );
glutInitWindowSize(, );
glutCreateWindow("太阳,地球和月亮"); // 改了窗口标题
glutDisplayFunc(&myDisplay);
glutIdleFunc(&myIdle); // 新加入了这句
glutMainLoop();
return ;
}

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的内容,所以我不打算详细讲述它。

#include <time.h>
double CalFrequency()
{
static int count;
static double save;
static clock_t last, current;
double timegap; ++count;
if( count <= )
return save;
count = ;
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);
最后的一步,也被我们解决了——虽然做法不太雅观,没关系,以后我们还会改善它的。

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

#include <GL/glut.h>
#include <stdio.h>
#include <time.h> // 太阳、地球和月亮
// 假设每个月都是12天
// 一年12个月,共是360天
static int day = ; // day的变化:从0到359 double CalFrequency()
{
static int count;
static double save;
static clock_t last, current;
double timegap; ++count;
if( count <= )
return save;
count = ;
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(, , , );
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
gluLookAt(, -, , , , , , , ); // 绘制红色的“太阳”
glColor3f(1.0f, 0.0f, 0.0f);
glutSolidSphere(, , );
// 绘制蓝色的“地球”
glColor3f(0.0f, 0.0f, 1.0f);
glRotatef(day/360.0*360.0, 0.0f, 0.0f, -1.0f);
glTranslatef(, 0.0f, 0.0f);
glutSolidSphere(, , );
// 绘制黄色的“月亮”
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(, 0.0f, 0.0f);
glutSolidSphere(, , ); glFlush();
glutSwapBuffers();
} void myIdle(void)
{
++day;
if( day >= )
day = ;
myDisplay();
} int main(int argc, char *argv[])
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE);
glutInitWindowPosition(, );
glutInitWindowSize(, );
glutCreateWindow("太阳,地球和月亮");
glutDisplayFunc(&myDisplay);
glutIdleFunc(&myIdle);
glutMainLoop();
return ;
}

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

OpenGL学习--------动画制作的更多相关文章

  1. OpenGL学习进程(10)第七课:四边形绘制与动画基础

        本节是OpenGL学习的第七个课时,下面以四边形为例介绍绘制OpenGL动画的相关知识:     (1)绘制几种不同的四边形: 1)四边形(GL_QUADS) OpenGL的GL_QUADS图 ...

  2. OpenGL学习之路(四)

    1 引子 上次读书笔记主要是学习了应用三维坐标变换矩阵对二维的图形进行变换,并附带介绍了GLSL语言的编译.链接相关的知识,之后介绍了GLSL中变量的修饰符,着重介绍了uniform修饰符,来向着色器 ...

  3. OpenGL学习进程(12)第九课:矩阵乘法实现3D变换

    本节是OpenGL学习的第九个课时,下面将详细介绍OpenGL的多种3D变换和如何操作矩阵堆栈.     (1)3D变换: OpenGL中绘制3D世界的空间变换包括:模型变换.视图变换.投影变换和视口 ...

  4. OpenGL学习之路(五)

    1 引子 不知不觉我们已经进入到读书笔记(五)了,我们先对前四次读书笔记做一个总结.前四次读书笔记主要是学习了如何使用OpenGL来绘制几何图形(包括二维几何体和三维几何体),并学习了平移.旋转.缩放 ...

  5. ios 学习动画的套路 (一)

    你也肯定喜欢炫酷的动画! 在APP中,动画就是一个点睛之笔!可以给用户增加一些独特的体验感,估计也有许多的和我一样的,看着那些觉得不错的动画,也就只能流口水的孩子,毕竟~不知道从哪里下手去写!会连续的 ...

  6. OpenGL学习进程(4)第二课:绘制图形

    本节是OpenGL学习的第二个课时,下面介绍如何用点和线来绘制图形:     (1)用点的坐标来绘制矩形: #include <GL/glut.h> void display(void) ...

  7. OpenGL学习进程(3)第一课:初始化窗体

        本节是OpenGL学习的第一个课时,下面介绍如何初始化一个窗体:     (1)显示一个有蓝色背景的窗体: #include <GL/glut.h> #include <st ...

  8. Spine应用--使用Spine动画制作动作游戏

    在前面的文章中,已经陆陆续续的讲解了一些使用Spine动画的细节,有了这些细节,我们已经满足了在unity中使用Spine动画制作动作游戏的技术基础. 那么,要使用Spine动画在unity中制作一款 ...

  9. CSS3动画制作

    CSS3动画制作 rotate 绕中心旋转 fadeInPendingFadeOutUp 先渐现,停留2s,再向上滑动并逐渐消失 fadeInUp2D 向上滑动并渐现, 因Animate.css的fa ...

随机推荐

  1. SCU 1069 POJ 2955 Brackets

    区间DP #include<cstdio> #include<cstring> #include<cmath> #include<algorithm> ...

  2. 观看网上的N多教程有感

    MD只想说一句,我擦. 长篇大论,有个叼毛用呀,显示你文采.... 糟粕真TMD多,直接简单的步骤多好,不要显示的你有多专业,其实就是一个二逼. 还有N多论坛,扯淡的人更多.

  3. DOM操作-引用同级的元素

    代码: ———————————————————————————————— <script type="text/javascript">            //获取 ...

  4. Eclipse配置--智能补全

    定位到:Windows→Preferences→Java→Editor→Content Assist 将Auto Activation triggers for java的默认值“.”修改为" ...

  5. allocator 类

    allcator是一个模板类 定义在memory头文件中,将内存分配与对象构造分开,分配的内存是原始的.未构造的 一.how to use 因其实一个类,则使用allcator时需要首先声明一个类对象 ...

  6. LeetCode OJ 235. Lowest Common Ancestor of a Binary Search Tree

    Given a binary search tree (BST), find the lowest common ancestor (LCA) of two given nodes in the BS ...

  7. LeetCode OJ 292.Nim Gam19. Remove Nth Node From End of List

    Given a linked list, remove the nth node from the end of list and return its head. For example, Give ...

  8. WisKey的眼神

    WisKey的眼神 Time Limit : 1000/1000ms (Java/Other)   Memory Limit : 32768/32768K (Java/Other) Total Sub ...

  9. mysql 常用命令集锦[绝对精华]

    一.连接MYSQL. 格式: mysql -h主机地址 -u用户名 -p用户密码 1.连接到本机上的MYSQL. 首先打开DOS窗口,然后进入目录mysql\bin,再键入命令mysql -u roo ...

  10. LightOJ 1370 Bi-shoe and Phi-shoe 数论

    题目大意:f(x)=n 代表1-x中与x互质的数字的个数.给出n个数字a[i],要求f(x)=a[i],求x的和. 思路:每个素数x 有x-1个不大于x的互质数.则f(x)=a[i],若a[i]+1为 ...