动画是由一副一副静止的画面组成的,通过在静止画面之间快速切换,比如1秒钟切换24副画面,而这24副画面之间的差别较小,这样我们的大脑会将前一副画面和后一副画面连接起来,形成平滑的动画。小时候的连环画,就有点动画的意思了,快速翻看连环画,就看到画面仿佛动起来了。
双缓冲
由于计算机程序没有那么多的纸张来描画静止画面,它只有一个显示屏,所以就导致两幅画面切换之前,需要擦除原来的画面,然后画上新的画面。这就导致一个问题,前一副画面只擦除了一半,然后后一副画面描画了一半,这样的情况是会出现的。这样的情况,通常会导致屏幕的闪烁。为了防止这种现象,就出现了双缓冲这个概念。
双缓冲,其实就是两个颜色 缓冲区,一个用来显示前一个画面,一个用来绘制后一个画面。假设有A,B,C三个画面,做成连环画。两个缓冲区,分别叫M和N好了。最开始的时候,在M缓冲区显示A画面,然后在N缓冲区,绘制B画面,当B画面绘制完毕后,交换M和N缓冲区,这时N缓冲区在前,M在后。接着在M缓冲区继续绘制C画面,当C画面绘制完毕后,又交换两个缓冲区。这样,每次看到的都是绘制完毕的画面了,这样就不会看到不完整的画面了。
最终结果如第二张图所示。当然,缓冲区M,N的名称是不会显示在图像中的,我们看到的最终就是 空图像->A->B->C。
暂停刷新
这个小节指的是每帧的绘制时间, 与视频刷新时间 不一致的时候,会出现什么样的情况。
每帧的绘制时间:每帧,就是指每一个静止的图像。要画一幅静止的图像,需要时间,每帧绘制时间,指的就是这个时间。
视频刷新时间: 每帧绘制完毕后,图像是放在缓冲区中,并没有真实显示出来。要显示出来,就需要由视频进行刷新。视频的刷新时间,是指视频将屏幕上的所有像素,刷新一次,需要的时间。视频的刷新时间,通常由视频刷新频率给出。
1. 每帧的绘制时间 < 视频刷新时间。 也就是说,缓冲区中的内容已经准备好了,视频还在刷新上一帧的数据。这其实可以接受,因为下一次视频刷新时,可以从缓冲区中取数据。
2. 每帧的绘制时间 > 视频刷新时间。 也就是说,我视频上一帧已经刷新完毕了,而缓冲区还没为我准备好下一帧的数据。这样视频刷新处于等待状态。这样的结果就是错过了视频刷新时间,只能等到下一次进行视频刷新了,这样画面就显得有点卡了。
3. 每帧的绘制时间 = 视频刷新时间。 按理说,这样的情况是最好的,谁也没等谁,两者同步了。但是,由于视频刷新时间是固定的,而每帧的绘制时间会有一些偏差。这样就会导致帧率的不稳定。帧率是指单位时间内的视频帧数。这样产生的效果就是,如果以一个人走路来打比方,就是走得时快时慢,并不匀速。
动画=重绘+交换
没什么可讲的了,直接看例子。
这个例子使用了双缓冲,使用双缓冲,有两处需要注意,一是main函数中的glutInitDisplayMode的参数,二是display函数末尾的glutSwapBuffers。
然后这个函数其他就是正常的绘制正方形。
只是多了一个正方形的旋转角度。spin这个角度在spinDisplay函数中增加,在display函数中使用。而spinDisplay函数,又是在mouse函数中指定为空闲函数。按下鼠标左键,当程序空闲时,就会调用spinDisplay,这个函数内部增加了spin的值,然后通过调用glutPostRedisplay函数,又让程序去调用display函数。这样,下一帧的正方形,就在上一帧的基础上,旋转了2度。只是现在的计算机足够快了,看起来这个正方形的旋转并不连贯。
#include <GL/glut.h>
#include <stdlib.h>
static GLfloat spin = 0.0; //正方形的旋转角度
void display(void)
{
glClear(GL_COLOR_BUFFER_BIT); //清除背景
glPushMatrix(); //模型视图矩阵 入栈
glRotatef(spin, 0.0, 0.0, 1.0); //指定模型绕Z轴旋转,Z轴指向屏幕外面
glColor3f(1.0, 1.0, 1.0); //白色矩形
glRectf(-25.0, -25.0, 25.0, 25.0); //矩形
glPopMatrix(); //模型视图矩阵出栈
glutSwapBuffers(); //交换缓冲区
}
void spinDisplay(void)
{
spin = spin + 2.0; //旋转角+2度
if (spin > 360.0)
spin = spin - 360.0; //避免旋转角超过360度
glutPostRedisplay(); //重新绘制,这导致display函数被调用
}
void init(void)
{
glClearColor(0.0, 0.0, 0.0, 0.0); //背景清除颜色为黑色
glShadeModel(GL_FLAT); //使用单色绘制物体
}
void reshape(int w, int h)
{
glViewport(0, 0, (GLsizei)w, (GLsizei)h); //指定视口大小
glMatrixMode(GL_PROJECTION); //进入投影矩阵
glLoadIdentity(); //投影矩阵设置为单位矩阵
glOrtho(-50.0, 50.0, -50.0, 50.0, -1.0, 1.0); //指定裁剪空间
glMatrixMode(GL_MODELVIEW); //返回模型视图矩阵
glLoadIdentity(); //模型视图矩阵设置为单位矩阵
}
void mouse(int button, int state, int x, int y)
{
switch (button) {
case GLUT_LEFT_BUTTON:
if (state == GLUT_DOWN)
glutIdleFunc(spinDisplay); //鼠标左键按下时,设置空闲函数为spinDisplay
break; //每当程序空闲时,就会调用spinDisplay函数
case GLUT_MIDDLE_BUTTON:
if (state == GLUT_DOWN)
glutIdleFunc(NULL); //鼠标中键按下时,设置空闲函数为空,空闲时不做任何事
break;
default:
break;
}
}
/*
* Request double buffer display mode.
* Register mouse input callback functions
*/
int main(int argc, char** argv)
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB); //使用双缓冲模式,使用RGB颜色模式
glutInitWindowSize(250, 250); //指定窗口大小
glutInitWindowPosition(100, 100); //窗口初始位置
glutCreateWindow(argv[0]); //创建窗口
init(); //初始化
glutDisplayFunc(display); //指定显示函数
glutReshapeFunc(reshape); //指定窗口尺寸变化时的函数
glutMouseFunc(mouse); //指定鼠标事件对应的处理函数
glutMainLoop(); //进入消息循环
return 0; /* ANSI C requires main to return int. */
}