OpenGL/GLUT实践:实现反弹运动的三角形动画与键盘控制(电子科技大学信软图形与动画Ⅱ实验)
源码见GitHub:A-UESTCer-s-Code
文章目录
- 1 运行效果
- 2 实验过程
- 2.1 环境配置
- 2.2 绘制三角形
- 2.2.1 渲染函数
- 2.2.2 主函数
- 2.2.3 运行结果
- 2.3 调整窗口大小
- 2.4 简单动画与按键控制
- 2.4.1 简单旋转
- 2.4.2 键盘控制
- 2.5 窗口反弹动画
- 2.5.1 处理窗口大小变化
- 2.5.2 渲染函数
- 2.5.3 定时器
- 2.5.4 控制速度
1 运行效果
我们运行程序,得到一个运动的等腰三角形,当其触碰到边框时会反弹,并且可以通过键盘上的F1、F2、F3来控制颜色,wasd来控制速度。
2 实验过程
2.1 环境配置
OpenGL作为图形显示库,在图形建模、游戏开发和科学可视化等领域有着广泛的应用,而GLUT作为其补充库则提供了方便的窗口管理和用户交互功能,简化了OpenGL程序的开发过程。
步骤:
-
下载Glut的依赖库:
- 访问OpenGL官网或直接从https://www.opengl.org/resources/libraries/glut/glut_downloads.php#windows下载Glut
- 解压下载文件,得到
glut.dll
、glut32.dll
、glut.lib
、glut32.lib
、glut.h
等5个文件
-
配置OpenGL环境:
-
将
glut.h
文件复制到 Visual Studio 安装目录下的 include 文件夹内的一个新建名为GL
的子文件夹中 -
将
glut32.lib
文件复制到 Visual Studio 安装目录下的lib文件夹中的x86
文件夹内 -
将
glut.lib
文件复制到 Visual Studio 安装目录下的lib文件夹中的x64
文件夹内 -
将
glut.dll
和glut32.dll
文件复制到系统文件夹C:\Windows\SysWOW64
中
-
-
创建OpenGL项目:
-
在Microsoft Visual Studio中创建一个控制台应用程序项目
-
添加源文件(如main.cpp)到项目中
-
在源文件中引入OpenGL和GLUT库:
#include <GL/glut.h>
-
-
编写测试程序:
- 编写一个简单的OpenGL程序,例如,绘制一个正方形
- 使用OpenGL提供的函数进行图形绘制
- 设置窗口的显示模式、大小、位置等参数
- 编译并运行程序,检查环境配置是否成功
运行结果:
2.2 绘制三角形
具体步骤:
2.2.1 渲染函数
void renderScene() {
glClear(GL_COLOR_BUFFER_BIT); // 清除颜色缓冲区
// 绘制三角形
glBegin(GL_TRIANGLES);
glColor3f(181./225, 206./225, 163./255); // 设置颜色为指定的RGB值
glVertex2f(-0.88, -0.5); // 左下角
glVertex2f(0.64, -0.5); // 右下角
glVertex2f(0.43, 0.7); // 顶点
glEnd();
glFlush(); // 刷新绘图命令
}
作用:renderScene()
函数是一个回调函数,在窗口需要被绘制时被调用,用于绘制图形。
细节讲解:
-
glClear(GL_COLOR_BUFFER_BIT);
:清除颜色缓冲区,确保每次绘制前都有一个干净的画布 -
glBegin(GL_TRIANGLES);
和glEnd();
:开始和结束绘制三角形的过程 -
glColor3f(181./225, 206./225, 163./255);
:设置绘制图形的颜色为指定的RGB值。需要注意的是,这里的颜色值是使用范围在0到1之间的小数来表示的,所以将RGB值除以255来进行归一化处理 -
glVertex2f()
:指定三角形的顶点坐标,这里使用的是二维坐标坐标是以窗口为中心, x x x 和 y y y 轴范围从
-1
到1
,三角形只需要设置三个顶点即可:glVertex2f(-0.88, -0.5); // 左下角 glVertex2f(0.64, -0.5); // 右下角 glVertex2f(0.43, 0.7); // 顶点
2.2.2 主函数
int main(int argc, char** argv) {
glutInit(&argc, argv); // 初始化GLUT库
glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB); // 设置显示模式为单缓冲和RGB颜色模式
glutInitWindowSize(400, 400); // 设置窗口大小
glutInitWindowPosition(100, 100); // 设置窗口位置
glutCreateWindow("Simple Triangle Test"); // 创建窗口,并设置标题
glutDisplayFunc(renderScene); // 设置绘图函数
glutMainLoop(); // 进入主循环,等待事件
return 0;
}
作用: main()
函数是程序的入口,主要负责初始化OpenGL环境和GLUT库,并进入主循环等待事件。
细节讲解:
glutInit(&argc, argv);
:初始化GLUT库,接受程序启动时的命令行参数glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB);
:设置显示模式为单缓冲和RGB颜色模式,这意味着窗口使用单缓冲区,并且颜色采用RGB模式glutInitWindowSize(400, 400);
:设置窗口的大小为400x400像素glutInitWindowPosition(100, 100);
:设置窗口的位置为屏幕左上角起始点向右下移动100像素的位置glutCreateWindow("Simple Triangle Test");
:创建窗口,并设置窗口标题为 “Simple Triangle Test”glutDisplayFunc(renderScene);
:注册回调函数renderScene()
,当窗口需要被重绘时会调用该函数进行绘制glutMainLoop();
:进入主循环,等待事件的发生,例如窗口的关闭、键盘输入等
2.2.3 运行结果
通过运行得到一个颜色为黄绿色(RGB为181, 206, 163),顶点为(0.43, 0.7)
、(0.64, -0.5)
、(-0.88, -0.5)
的普通三角形
2.3 调整窗口大小
当我们调整窗口时,图像出现了明显的变形,窗口大小改变会影响OpenGL的视口和投影矩阵,而默认情况下OpenGL使用的是透视投影矩阵,因此窗口大小改变会导致图像的变形。为了解决这个问题,我们需要重新计算投影矩阵,并将其与窗口的新大小相匹配。
在OpenGL中,我们可以使用 glutReshapeFunc()
函数来注册一个回调函数,以便在窗口大小改变时进行处理。这个函数的原型如下:
void glutReshapeFunc(void (*func)(int width, int height));
这个函数将一个自定义的函数与窗口大小改变事件绑定,当窗口大小改变时,该函数就会被调用。
接下来,我们需要编写一个自定义的函数来处理窗口大小改变事件。这个函数应该重新计算投影矩阵,并将其与新的窗口大小相匹配。下面是一个示例函数:
void changeSize(int width, int height) {
// 避免除以0
if (height == 0) height = 1;
// 设置视口大小
glViewport(0, 0, width, height);
// 设置投影矩阵
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(45.0f, (GLfloat)width / (GLfloat)height, 0.1f, 100.0f);
// 切换回模型观察矩阵
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
}
方法:
为了处理窗口大小的改变,我们使用了OpenGL提供的glutReshapeFunc()
函数来注册一个回调函数,该函数会在窗口大小改变时被调用。在这个回调函数中,我们重新计算投影矩阵,并根据新的窗口大小设置视口和透视投影。
具体而言,我们的处理过程包括以下步骤:
- 检查窗口的高度是否为0,如果是则将其设置为1,以避免除以0的错误
- 计算窗口的宽高比,以便在设置透视投影时使用
- 将当前矩阵模式设置为投影矩阵模式,并重置矩阵
- 设置OpenGL的视口大小为新的改变后的窗口大小
- 使用
gluPerspective()
函数设置透视投影,其中包括视角大小、宽高比、近裁剪面和远裁剪面的距离 - 将当前矩阵模式设置回模型视图矩阵模式,并重置矩阵
- 使用
gluLookAt()
函数设置观察者的视点和方向,以模拟相机在场景中的位置和方向
最后,在 main()
函数中,我们需要将这个自定义的函数与窗口大小改变事件绑定起来,如下所示:
glutReshapeFunc(changeSize);
注意:这个函数要在主循环
glutMainLoop();
前,否则无法生效(因为程序一直在主循环运行,运行不到这个函数)
通过这样的设置,当我们调整窗口大小时,OpenGL会自动调用 changeSize()
函数来重新计算投影矩阵,从而保证图像的正确显示
执行结果:
2.4 简单动画与按键控制
2.4.1 简单旋转
目的:
本部分旨在实现一个简单的OpenGL动画,让一个三角形在窗口中绕固定轴匀速旋转。
方法:
为了实现动画效果,我们需要做以下几个步骤:
-
在
main()
函数中,设置双缓冲区模式(GLUT_DOUBLE
)以及RGB颜色模式(GLUT_RGB
),这样可以避免画面闪烁并且产生平滑的动画效果双缓冲区通过在后一个缓冲区里绘画,并不停交换前后缓冲区(可见缓冲区),来产生平滑的动画。使用双缓冲区可以预防闪烁。
-
使用
glutSwapBuffers()
函数在每一帧绘制完毕后切换前后缓冲区,将后台缓冲区的内容绘制到屏幕上 -
使用
glutTimerFunc()
函数设置一个定时器,以一定的时间间隔调用指定的函数,在这个函数中更新动画的状态 -
在定时器函数中更新三角形的旋转角度,并重新注册定时器,实现连续的动画效果
GLfloat angle = 0;
:定义了一个全局变量angle
,用于表示旋转角度,初始值为0timer()
函数:是一个定时器函数,在每次被调用时更新旋转角度,并请求重绘窗口。这里使用了glutPostRedisplay()
函数请求重绘,以触发renderScene()
函数的执行。同时,通过glutTimerFunc()
函数设置了一个16毫秒的定时器,用于每隔一定时间调用一次timer()
函数,从而实现连续的动画效果
定时器函数:
GLfloat angle = 0; // 设置初始旋转角度 // 定时器函数,每16ms调用一次,用于旋转三角形 void timer(int value) { angle += 1; // 旋转角度 glutPostRedisplay(); // 重绘 glutTimerFunc(16, timer, 0); // 设置定时器 }
-
在渲染函数
renderScene()
中,使用更新后的旋转角度绘制旋转的三角形renderScene()
函数:在这里,首先清除颜色缓冲区,然后将当前矩阵推入堆栈,接着根据当前的旋转角度对场景进行旋转,绘制一个黄绿色的三角形。最后,通过glutSwapBuffers()
函数进行双缓冲区的切换,将后台缓冲区的内容显示到屏幕上。// 渲染函数,绘制一个单色的三角形 void renderScene() { glClear(GL_COLOR_BUFFER_BIT); // 清除颜色缓冲区 glPushMatrix(); glRotatef(angle, 1.0, 0.0, 0.0); // 绕x旋转 // 绘制三角形 // ... glPopMatrix(); glutSwapBuffers(); // 设置双缓冲 }
结果:
通过以上步骤,我们可以实现一个简单的OpenGL动画,让一个三角形在窗口中不断旋转。动画效果平滑流畅。
2.4.2 键盘控制
普通按键处理:
使用 glutKeyboardFunc
函数来注册处理普通按键事件的回调函数。
-
普通按键是指字母,数字和其他可以用 ASCII 代码表示的键
-
我们可以在普通按键回调函数中执行相应的操作,例如根据按下的键执行不同的操作,或者响应程序的退出等
void processNormalKeys(unsigned char key, int x, int y) { switch (key) { case 27: // ESC键 exit(0); break; }
特殊按键处理:
使用 glutSpecialFunc
函数来注册处理特殊按键事件的回调函数。
-
特殊按键的标识符通常由预定义的常量表示,如
GLUT_KEY_F1
、GLUT_KEY_LEFT
等 -
我们可以在特殊按键回调函数中根据按下的特殊键执行相应的操作,例如改变颜色、移动物体等
记得将颜色值设为全局变量,渲染时,使用全局变量对颜色进行设置
void processSpecialKeys(int key, int x, int y) { switch (key) { case GLUT_KEY_F1: red = 181. / 225; green = 206. / 225; blue = 163. / 255; break; case GLUT_KEY_F2: red = 163. / 255; green = 163. / 255; blue = 163. / 255; break; case GLUT_KEY_F3: red = 30. / 255; green = 60. / 255; blue = 153. / 255; break; }
最后在main
函数里面设置这两个回调函数即可
int main(int argc, char** argv)
{
// ...
glutSpecialFunc(processSpecialKeys); // 设置特殊键回调函数
glutKeyboardFunc(processNormalKeys); // 设置普通键回调函数
// ...
}
运行结果:
- 对
processSpecialKeys
函数,根据按下的特殊键设置不同的颜色值。具体地,当按下F1键时,设置颜色为黄绿色;按下F2键时,设置颜色为灰色;按下F3键时,设置颜色为蓝色 - 对
processNormalKeys
函数,增加对ESC键的处理。当按下ESC键时,退出程序
2.5 窗口反弹动画
这一部分,我们要实现可以反弹运动的三角形动画,键盘控制三角形移动速度、颜色。
先引入几个全局变量:
GLfloat x = 0, y = 0, rsize = 50; // 设置初始位置和大小
GLfloat dx = 1.5, dy = 1.5; // 设置初始速度
GLfloat windowWidth, windowHeight; // 窗口大小
GLfloat red = 181. / 225, green = 206. / 225, blue = 163. / 255; // 设置初始颜色为黄绿色
2.5.1 处理窗口大小变化
-
函数调用
glViewport
函数设置新的视口大小glViewport(0, 0, w, h); // 设置视口大小
-
函数计算新的宽高比,并根据宽高比的大小调整窗口的宽度和高度。如果宽度小于或等于高度,那么窗口的宽度被设置为100,高度则根据宽高比进行调整。否则,窗口的高度被设置为100,宽度则根据宽高比进行调整
aspectRatio = (GLfloat)w / (GLfloat)h; // 计算窗口的宽高比 if (w <= h) { windowWidth = 100; windowHeight = 100 / aspectRatio; } else { windowWidth = 100 * aspectRatio; windowHeight = 100; }
引入了
windowWidth
和windowHeight
全局变量,用于保存窗口的宽度和高度 -
使用
glOrtho
函数设置正交投影矩阵,将场景限定在一个固定大小的区域内,以保持图形的比例不变glOrtho(-windowWidth, windowWidth, -windowHeight, windowHeight, 1.0, -1.0);
2.5.2 渲染函数
渲染函数修改:
-
在绘制三角形时,顶点坐标根据
x
和y
的值确定,以及矩形大小rsize
的一半,顶点定义如下:glVertex2f(x, y); glVertex2f(x + rsize, y); glVertex2f(x + rsize / 2, y + rsize);
2.5.3 定时器
修改了定时器函数 timer
:
-
先更新位置
x
和y
x += dx; y += dy;
-
然后检测是否与窗口边界发生碰撞,如果碰撞,则将对应的速度分量
dx
或dy
取反,实现反弹效果,并稍微调整位置以避免卡在边界上// 检测碰撞,如果碰到边界就反弹 if (x + rsize > windowWidth || x < -windowWidth) { dx = -dx; x += dx; // 稍微调整位置,防止卡在边界上 } if (y + rsize > windowHeight || y < -windowHeight) { dy = -dy; y += dy; // 稍微调整位置,防止卡在边界上 }
-
使用
glutPostRedisplay()
触发重绘,以更新窗口中的图形 -
设置定时器每16ms触发一次
glutTimerFunc(16, timer, 1);
,以实现动画效果
2.5.4 控制速度
控制速度的实现:
我们使用了 processNormalKeys
函数来处理普通按键事件
- 当按下 ESC 键时,程序退出
- 当按下 ‘w’ 键时,增加图形在y方向上的速度
- 当按下 ‘s’ 键时,减少图形在y方向上的速度,但保证速度不小于0
- 当按下 ‘a’ 键时,减少图形在x方向上的速度,但保证速度不小于0
- 当按下 ‘d’ 键时,增加图形在x方向上的速度
switch (key)
{
case 27: // ESC键
exit(0);
break;
case 'w': // 增加y方向的速度
dy += 0.1;
break;
case 's': // 减少y方向的速度,但不小于0
if (dy > 0)
{
dy -= 0.1;
}
break;
case 'a': // 减少x方向的速度,但不小于0
if (dx > 0)
{
dx -= 0.1;
}
break;
case 'd': // 增加x方向的速度
dx += 0.1;
break;
}