Skip to content

OpenGL/GLUT实践:实现反弹运动的三角形动画与键盘控制

3984字约13分钟

OpenGLCG

2024-09-02

源码见GitHub:A-UESTCer-s-Code

1 运行效果

我们运行程序,得到一个运动的等腰三角形,当其触碰到边框时会反弹,并且可以通过键盘上的F1、F2、F3来控制颜色,wasd来控制速度。

recording

2 实现过程

2.1 环境配置

OpenGL作为图形显示库,在图形建模、游戏开发和科学可视化等领域有着广泛的应用,而GLUT作为其补充库则提供了方便的窗口管理和用户交互功能,简化了OpenGL程序的开发过程。

步骤:

  1. 下载Glut的依赖库:

    • 访问OpenGL官网或直接从https://www.opengl.org/resources/libraries/glut/glut_downloads.php#windows下载Glut
    • 解压下载文件,得到glut.dllglut32.dllglut.libglut32.libglut.h5个文件
  2. 配置OpenGL环境:

    • glut.h文件复制到 Visual Studio 安装目录下的 include 文件夹内的一个新建名为 GL 的子文件夹中

      image-20240330085410414
    • glut32.lib文件复制到 Visual Studio 安装目录下的lib文件夹中的x86文件夹内

      image-20240330085440530
    • glut.lib文件复制到 Visual Studio 安装目录下的lib文件夹中的x64文件夹内

      image-20240330085509954
    • glut.dllglut32.dll文件复制到系统文件夹 C:\Windows\SysWOW64

      image-20240330085332324
  3. 创建OpenGL项目:

    • 在Microsoft Visual Studio中创建一个控制台应用程序项目

    • 添加源文件(如main.cpp)到项目中

    • 在源文件中引入OpenGL和GLUT库:#include <GL/glut.h>

  4. 编写测试程序:

    • 编写一个简单的OpenGL程序,例如,绘制一个正方形
    • 使用OpenGL提供的函数进行图形绘制
    • 设置窗口的显示模式、大小、位置等参数
    • 编译并运行程序,检查环境配置是否成功

    运行结果:

    image-20240330091416015

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():指定三角形的顶点坐标,这里使用的是二维坐标

    坐标是以窗口为中心,xxyy 轴范围从-11,三角形只需要设置三个顶点即可:

    glVertex2f(-0.88, -0.5); // 左下角
    glVertex2f(0.64, -0.5); // 右下角
    glVertex2f(0.43, 0.7); // 顶点
    image-20240330092749677

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)的普通三角形

image-20240330093833522

2.3 调整窗口大小

当我们调整窗口时,图像出现了明显的变形,窗口大小改变会影响OpenGL的视口和投影矩阵,而默认情况下OpenGL使用的是透视投影矩阵,因此窗口大小改变会导致图像的变形。为了解决这个问题,我们需要重新计算投影矩阵,并将其与窗口的新大小相匹配。

image-20240330094129019

在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()函数来注册一个回调函数,该函数会在窗口大小改变时被调用。在这个回调函数中,我们重新计算投影矩阵,并根据新的窗口大小设置视口和透视投影。

具体而言,我们的处理过程包括以下步骤:

  1. 检查窗口的高度是否为0,如果是则将其设置为1,以避免除以0的错误
  2. 计算窗口的宽高比,以便在设置透视投影时使用
  3. 将当前矩阵模式设置为投影矩阵模式,并重置矩阵
  4. 设置OpenGL的视口大小为新的改变后的窗口大小
  5. 使用gluPerspective()函数设置透视投影,其中包括视角大小、宽高比、近裁剪面和远裁剪面的距离
  6. 将当前矩阵模式设置回模型视图矩阵模式,并重置矩阵
  7. 使用gluLookAt()函数设置观察者的视点和方向,以模拟相机在场景中的位置和方向

最后,在 main() 函数中,我们需要将这个自定义的函数与窗口大小改变事件绑定起来,如下所示:

glutReshapeFunc(changeSize);

注意:这个函数要在主循环 glutMainLoop();,否则无法生效(因为程序一直在主循环运行,运行不到这个函数)

通过这样的设置,当我们调整窗口大小时,OpenGL会自动调用 changeSize() 函数来重新计算投影矩阵,从而保证图像的正确显示

执行结果:

recording

2.4 简单动画与按键控制

2.4.1 简单旋转

目的:

本部分旨在实现一个简单的OpenGL动画,让一个三角形在窗口中绕固定轴匀速旋转。

方法:

为了实现动画效果,我们需要做以下几个步骤:

  1. main()函数中,设置双缓冲区模式(GLUT_DOUBLE)以及RGB颜色模式(GLUT_RGB),这样可以避免画面闪烁并且产生平滑的动画效果

    双缓冲区通过在后一个缓冲区里绘画,并不停交换前后缓冲区(可见缓冲区),来产生平滑的动画。使用双缓冲区可以预防闪烁。

  2. 使用glutSwapBuffers()函数在每一帧绘制完毕后切换前后缓冲区,将后台缓冲区的内容绘制到屏幕上

  3. 使用glutTimerFunc()函数设置一个定时器,以一定的时间间隔调用指定的函数,在这个函数中更新动画的状态

  4. 在定时器函数中更新三角形的旋转角度,并重新注册定时器,实现连续的动画效果

    • GLfloat angle = 0;:定义了一个全局变量angle,用于表示旋转角度,初始值为0
    • timer()函数:是一个定时器函数,在每次被调用时更新旋转角度,并请求重绘窗口。这里使用了glutPostRedisplay()函数请求重绘,以触发renderScene()函数的执行。同时,通过glutTimerFunc()函数设置了一个16毫秒的定时器,用于每隔一定时间调用一次timer()函数,从而实现连续的动画效果

    定时器函数

    GLfloat angle = 0; // 设置初始旋转角度
    // 定时器函数,每16ms调用一次,用于旋转三角形
    void timer(int value)
    {
        angle += 1; // 旋转角度
        glutPostRedisplay(); // 重绘
        glutTimerFunc(16, timer, 0); // 设置定时器
    }
  5. 在渲染函数renderScene()中,使用更新后的旋转角度绘制旋转的三角形

    renderScene()函数:在这里,首先清除颜色缓冲区,然后将当前矩阵推入堆栈,接着根据当前的旋转角度对场景进行旋转,绘制一个黄绿色的三角形。最后,通过glutSwapBuffers()函数进行双缓冲区的切换,将后台缓冲区的内容显示到屏幕上。

    // 渲染函数,绘制一个单色的三角形
    void renderScene()
    {
        glClear(GL_COLOR_BUFFER_BIT); // 清除颜色缓冲区
        glPushMatrix();
        glRotatef(angle, 1.0, 0.0, 0.0); // 绕x旋转
        // 绘制三角形
        // ...
        glPopMatrix(); 
        glutSwapBuffers(); // 设置双缓冲
    }

结果: recording

通过以上步骤,我们可以实现一个简单的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_F1GLUT_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); // 设置普通键回调函数
    // ...
}

运行结果:

recording
  • 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;
    }

    引入了 windowWidthwindowHeight 全局变量,用于保存窗口的宽度和高度

  • 使用 glOrtho 函数设置正交投影矩阵,将场景限定在一个固定大小的区域内,以保持图形的比例不变

     glOrtho(-windowWidth, windowWidth, -windowHeight, windowHeight, 1.0, -1.0);

2.5.2 渲染函数

渲染函数修改:

  • 在绘制三角形时,顶点坐标根据 xy 的值确定,以及矩形大小 rsize 的一半,顶点定义如下:

    glVertex2f(x, y);     
    glVertex2f(x + rsize, y);
    glVertex2f(x + rsize / 2, y + rsize);

2.5.3 定时器

修改了定时器函数 timer

  • 先更新位置 xy

    x += dx;
    y += dy;
  • 然后检测是否与窗口边界发生碰撞,如果碰撞,则将对应的速度分量 dxdy 取反,实现反弹效果,并稍微调整位置以避免卡在边界上

     // 检测碰撞,如果碰到边界就反弹
     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 函数来处理普通按键事件

  1. 当按下 ESC 键时,程序退出
  2. 当按下 'w' 键时,增加图形在y方向上的速度
  3. 当按下 's' 键时,减少图形在y方向上的速度,但保证速度不小于0
  4. 当按下 'a' 键时,减少图形在x方向上的速度,但保证速度不小于0
  5. 当按下 '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;
}