• Qt OpenGL 粒子系统


    这次教程中,我们将创建一个简单的粒子系统,并用它来创建一种喷射效果。利用粒子系统,我们可以实现爆炸、喷泉、流星之类的效果,听起来是不是很棒呢!

    我们还会讲到一个新东西,三角形带(我的理解就是画很多三角形来组合成我们要的形状),它非常容易使用,而且当需要画很多三角形的时候,它能加快你程序的运行速度。这次教程中,我将教你该如何做一个简单的微粒程序,一旦你了解微粒程序的原理后,再创建例如:火、烟、喷泉等效果将是很轻松的事情。

    程序运行时效果如下:

    下面进入教程:

    我们这次将在第06课代码的基础上修改代码,这次需要修改的代码量不少,希望大家耐心跟着我一步步来完成这个程序。首先打开myglwidget.h文件,将类声明更改如下:

     1 #ifndef MYGLWIDGET_H
     2 #define MYGLWIDGET_H
     3  
     4 #include <QWidget>
     5 #include <QGLWidget>
     6  
     7 class MyGLWidget : public QGLWidget
     8 {
     9     Q_OBJECT
    10 public:
    11     explicit MyGLWidget(QWidget *parent = 0);
    12     ~MyGLWidget();
    13  
    14 protected:
    15     //对3个纯虚函数的重定义
    16     void initializeGL();
    17     void resizeGL(int w, int h);
    18     void paintGL();
    19  
    20     void keyPressEvent(QKeyEvent *event);           //处理键盘按下事件
    21  
    22 private:
    23     bool fullscreen;                                //是否全屏显示
    24     QString m_FileName;                             //图片的路径及文件名
    25     GLuint m_Texture;                               //储存一个纹理
    26  
    27     static const int MAX_PARTICLES = 1000;          //最大粒子数
    28     static const GLfloat COLORS[12][3];             //彩虹的颜色
    29     bool m_Rainbow;                                 //是否为彩虹模式
    30     GLuint m_Color;                                 //当前的颜色
    31  
    32     float m_Slowdown;                               //减速粒子
    33     float m_xSpeed;                                 //x方向的速度
    34     float m_ySpeed;                                 //y方向的速度
    35     float m_Deep;                                   //移入屏幕的距离
    36  
    37     struct Particle                                 //创建粒子结构体
    38     {
    39         bool active;                                //是否激活
    40         float life;                                 //粒子生命
    41         float fade;                                 //衰减速度
    42  
    43         float r, g, b;                              //粒子颜色
    44         float x, y, z;                              //位置坐标
    45         float xi, yi, zi;                           //各方向速度
    46         float xg, yg, zg;                           //各方向加速度
    47     } m_Particles[MAX_PARTICLES];                   //存放1000个粒子的数组
    48 };
    49  
    50 #endif // MYGLWIDGET_H

    首先我们定义了一个静态整形常量MAX_PARTICLES来存放粒子的最大数目,和一个静态GLfloat常量数组来存放彩虹的颜色。接着是一个布尔变量m_Rainbow来表示当前模式是否为彩虹模式,然后是GLuint变量m_Color来表示当前的粒子的颜色,它将在控制粒子颜色在彩虹颜色数组中切换。粒子颜色会与纹理融合,我们用纹理而不用电的重要原因是,点的速度慢,而且挺麻烦的,其次纹理很酷,也好控制。

    下面四行是定义了四个浮点变量。m_Slowdown控制粒子移动的快慢,数值越高移动越快,数值越低移动越慢,粒子的速度将影响它们在屏幕上移动的距离,要注意速度慢的粒子不会移动很远就会消失。m_xSpeed和m_ySpeed控制尾部的方向,m_xSpeed为正时粒子将会向右移动,负时则向左移动,m_ySpeed为正时粒子将会向上移动,负时则向下移动,m_xSpeed和m_ySpeed有助于在我们想要的方向上移动粒子。最后是变量m_Deep,我们用该变量移入移除我们的屏幕,在粒子系统中,有时当接近你时,可以看见更多美妙的图像。

    最后我们定义了结构体Particle,用来描述某一粒子的状态属性。我们用布尔变量active开始,如果为true,我们的粒子为活跃的;如果为false则粒子为死的,此时我们就不绘制它。变量life和fade来控制粒子显示多久以及显示时候的亮度,随着life数值的降低fade的数值也相应减低,这将导致一些粒子比其他粒子燃烧的时间长。后面是记录粒子颜色,位置,速度,加速度等状态属性的变量,作用我想大家会点高中物理都能明白的,最后我们创建一个长度为MAX_PARTICLES的结构体数组。

    接下来,我们打开myglwidget.cpp,在构造函数中对新增变量进行初始化,具体代码如下:

     1 const GLfloat MyGLWidget::COLORS[][3] =                 //彩虹的颜色
     2 {
     3     {1.0f, 0.5f, 0.5f}, {1.0f, 0.75f, 0.5f}, {1.0f, 1.0f, 0.5f},
     4     {0.75f, 1.0f, 0.5f}, {0.5f, 1.0f, 0.5f}, {0.5f, 1.0f, 0.75f},
     5     {0.5f, 1.0f, 1.0f}, {0.5f, 0.75f, 1.0f}, {0.5f, 0.5f, 1.0f},
     6     {0.75f, 0.5f, 1.0f}, {1.0f, 0.5f, 1.0f}, {1.0f, 0.5f, 0.75f}
     7 };
     8  
     9 MyGLWidget::MyGLWidget(QWidget *parent) :
    10     QGLWidget(parent)
    11 {
    12     fullscreen = false;
    13     m_FileName = "D:/QtOpenGL/QtImage/Particle.bmp";    //应根据实际存放图片的路径进行修改
    14     m_Rainbow = true;
    15     m_Color = 0;
    16     m_Slowdown = 2.0f;
    17     m_xSpeed = 0.0f;
    18     m_ySpeed = 0.0f;
    19     m_Deep = -40.0f;
    20  
    21     for (int i=0; i<MAX_PARTICLES; i++)                 //循环初始化所以粒子
    22     {
    23         m_Particles[i].active = true;                   //使所有粒子为激活状态
    24         m_Particles[i].life = 1.0f;                     //所有粒子生命值为最大
    25         //随机生成衰减速率
    26         m_Particles[i].fade = float(rand()%100)/1000.0f+0.001;
    27  
    28         //粒子的颜色
    29         m_Particles[i].r = COLORS[int(i*(12.0f/MAX_PARTICLES))][0];
    30         m_Particles[i].g = COLORS[int(i*(12.0f/MAX_PARTICLES))][1];
    31         m_Particles[i].b = COLORS[int(i*(12.0f/MAX_PARTICLES))][2];
    32  
    33         //粒子的初始位置
    34         m_Particles[i].x = 0.0f;
    35         m_Particles[i].y = 0.0f;
    36         m_Particles[i].z = 0.0f;
    37  
    38         //随机生成x、y、z轴方向速度
    39         m_Particles[i].xi = float((rand()%50)-26.0f)*10.0f;
    40         m_Particles[i].yi = float((rand()%50)-25.0f)*10.0f;
    41         m_Particles[i].zi = float((rand()%50)-25.0f)*10.0f;
    42  
    43         m_Particles[i].xg = 0.0f;                       //设置x方向加速度为0
    44         m_Particles[i].yg = -0.8f;                      //设置y方向加速度为-0.8
    45         m_Particles[i].zg = 0.0f;                       //设置z方向加速度为0
    46     }
    47  
    48     QTimer *timer = new QTimer(this);                   //创建一个定时器
    49     //将定时器的计时信号与updateGL()绑定
    50     connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
    51     timer->start(10);                                   //以10ms为一个计时周期
    52 }

    注意到我们在构造函数之前对定义的静态常量数组COLORS进行初始化,一共包含12种渐变颜色,从红色到紫罗兰。进入构造函数一开始是更换纹理图片以及增加变量的初始化,这些没什么好解释的,下面我们重点看循环部分。我们利用循环来初始化每个粒子,我们让粒子变活跃(不活跃的粒子在屏幕上是不会显示的)之后,我们给它lfie。life满值是1.0f,这也给粒子完整的光亮。值得一提,把粒子的生命衰退和颜色渐暗绑到一起,效果真的很不错!

    我们通过随机数来设置粒子退色的快慢,我们取0~99的随机数,然后平分1000份来得到一个很小的浮点数,最后结果加上0.001f来使fade速度值不为0。我们既然给了粒子生命,我们当然要给它其他的属性状态附上值,为了使粒子有不同的颜色,我们用i 变量乘以数组中颜色的数目(12)与MAX_PARTICLES的商,再转换成整数,利用得到的整数取对应的颜色就可以了。然后让粒子从(0, 0, 0)出发,在设定速度时,我们通过将结果乘上10.0f来创造开始时的爆炸效果,加速度就由我们统一指定初始值了。

    然后,我们来略微修改initializeGL()函数,代码如下:

     1 void MyGLWidget::initializeGL()                         //此处开始对OpenGL进行所以设置
     2 {
     3     m_Texture = bindTexture(QPixmap(m_FileName));       //载入位图并转换成纹理
     4     glEnable(GL_TEXTURE_2D);                            //启用纹理映射
     5  
     6     glClearColor(0.0f, 0.0f, 0.0f, 0.0f);               //黑色背景
     7     glShadeModel(GL_SMOOTH);                            //启用阴影平滑
     8     glClearDepth(1.0);                                  //设置深度缓存
     9     glDisable(GL_DEPTH_TEST);                           //禁止深度测试
    10     glEnable(GL_BLEND);                                 //启用融合
    11     glBlendFunc(GL_SRC_ALPHA, GL_ONE);                  //设置融合因子
    12     glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);  //告诉系统对透视进行修正
    13     glHint(GL_POINT_SMOOTH_HINT, GL_NICEST);
    14 }

    我们在中间启用了融合并设置了融合因子,这是为了我们的粒子能有不同颜色。然后我们禁用了深度测试,因为如果启用深度测试的话,纹理之间会出现覆盖现象,那样画面简直一团糟。

    还有,我们要进入有趣的paintGL()函数了,具体代码如下:

     1 void MyGLWidget::paintGL()                              //从这里开始进行所以的绘制
     2 {
     3     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存
     4     glLoadIdentity();                                   //重置模型观察矩阵
     5     glBindTexture(GL_TEXTURE_2D, m_Texture);
     6  
     7     for (int i=0; i<MAX_PARTICLES; i++)                 //循环所以的粒子
     8     {
     9         if (m_Particles[i].active)                      //如果粒子为激活的
    10         {
    11             float x = m_Particles[i].x;                 //x轴位置
    12             float y = m_Particles[i].y;                 //y轴位置
    13             float z = m_Particles[i].z + m_Deep;        //z轴位置
    14             //设置粒子颜色
    15             glColor4f(m_Particles[i].r, m_Particles[i].g,
    16                       m_Particles[i].b, m_Particles[i].life);
    17             glBegin(GL_TRIANGLE_STRIP);                 //绘制三角形带
    18                 glTexCoord2d(1, 1);glVertex3f(x+0.5f, y+0.5f, z);
    19                 glTexCoord2d(0, 1);glVertex3f(x-0.5f, y+0.5f, z);
    20                 glTexCoord2d(1, 0);glVertex3f(x+0.5f, y-0.5f, z);
    21                 glTexCoord2d(0, 0);glVertex3f(x-0.5f, y-0.5f, z);
    22             glEnd();
    23  
    24             //更新各方向坐标及速度
    25             m_Particles[i].x += m_Particles[i].xi/(m_Slowdown*1000);
    26             m_Particles[i].y += m_Particles[i].yi/(m_Slowdown*1000);
    27             m_Particles[i].z += m_Particles[i].zi/(m_Slowdown*1000);
    28             m_Particles[i].xi += m_Particles[i].xg;
    29             m_Particles[i].yi += m_Particles[i].yg;
    30             m_Particles[i].zi += m_Particles[i].zg;
    31  
    32             m_Particles[i].life -= m_Particles[i].fade; //减少粒子的生命值
    33             if (m_Particles[i].life < 0.0f)             //如果粒子生命值小于0
    34             {
    35                 m_Particles[i].life = 1.0f;             //产生一个新粒子
    36                 m_Particles[i].fade = float(rand()%100)/1000.0f+0.003f;
    37  
    38                 m_Particles[i].r = colors[m_Color][0];  //设置颜色
    39                 m_Particles[i].g = colors[m_Color][1];
    40                 m_Particles[i].b = colors[m_Color][2];
    41  
    42                 m_Particles[i].x = 0.0f;                //粒子出现在屏幕中央
    43                 m_Particles[i].y = 0.0f;
    44                 m_Particles[i].z = 0.0f;
    45  
    46                 //随机生成粒子速度
    47                 m_Particles[i].xi = m_xSpeed + float((rand()%60)-32.0f);
    48                 m_Particles[i].yi = m_ySpeed + float((rand()%60)-30.0f);
    49                 m_Particles[i].zi = float((rand()%60)-30.0f);
    50             }
    51         }
    52     }
    53  
    54     if (m_Rainbow)                                      //如果为彩虹模式
    55     {
    56         m_Color++;                                      //进行颜色的变换
    57         if (m_Color > 11)
    58         {
    59             m_Color = 0;
    60         }
    61     }
    62 }

    paintGL()函数中,我们在循环中没有重置模型观察矩阵,因为我们并没有使用过glRotate和glTranslate函数,我们在画粒子位置的时候,计算出相应坐标,用glVertex3f()函数来代替glTranslate函数,这样在我们画粒子的时候就不会改变模型观察矩阵了。

    然后我们建立一个循环,在循环中更新绘制每一个粒子。首先检查粒子是否活跃,如果不活跃则不被更新(在这个程序中,它们始终都是活跃的)。接着定义三个临时变量存放粒子的x、y、z值,设置粒子颜色,然后就来绘制它了,我们用一个三角形带来代替四边形这样使程序运行快一点(一般情况是这样,关于三角形带点此有相关文章)。

    接下来我们来移动粒子。首先我们取得当前粒子的x位置,然后把x运动速度加上粒子被减速1000倍后的值。所以如果粒子在x轴(0)上屏幕中心的位置,x轴速度(xi)为+10,而m_Slowdown为1,我们可以以10/(1*1000)或0.01f速度移向右边。如果,m_slowDown值到2我们的速度就只有0.005f了。这也是为什么yong10.0f乘开始值来叫像素移动快速,制造一个爆发效果。然后我们要根据加速度更新我们粒子的速度,根据衰退速度更新我们粒子的生命。

    最后我们检查粒子是否还活着(生命值大于0),如果粒子烧尽,我们会使它恢复,我们给它满值生命和新的衰退速度。当然我们也重新设定粒子回到屏幕中心,然后重新随机生成速度。要注意,我们没有将移动速度乘10,我们这次不想要一个爆发效果,而要比较慢地移动粒子;然后我们要相应的加上m_xSpeed和m_ySpeed,这个控制了粒子大体得移动方向。最后我们给粒子分配当前的颜色就搞定循环了。

    函数最后,我们判断是否为彩虹模式,如果是就改变当前的颜色,这样不同时间“重生”后的粒子就可能得到不同的颜色,从而出现彩虹效果。

    最后就是键盘控制了,由于为了增加点趣味性,这次键盘控制比较“麻烦”,但是调理很清晰,具体代码如下:

      1 void MyGLWidget::keyPressEvent(QKeyEvent *event)
      2 {
      3     switch (event->key())
      4     {
      5     case Qt::Key_F1:                                    //F1为全屏和普通屏的切换键
      6         fullscreen = !fullscreen;
      7         if (fullscreen)
      8         {
      9             showFullScreen();
     10         }
     11         else
     12         {
     13             showNormal();
     14         }
     15         updateGL();
     16         break;
     17     case Qt::Key_Escape:                                //ESC为退出键
     18         close();
     19         break;
     20     case Qt::Key_Tab:                                   //Tab按下使粒子回到原点,产生爆炸
     21         for (int i=0; i<MAX_PARTICLES; i++)
     22         {
     23             m_Particles[i].x = 0.0f;
     24             m_Particles[i].y = 0.0f;
     25             m_Particles[i].z = 0.0f;
     26  
     27             //随机生成速度
     28             m_Particles[i].xi = float((rand()%50)-26.0f)*10.0f;
     29             m_Particles[i].yi = float((rand()%50)-25.0f)*10.0f;
     30             m_Particles[i].zi = float((rand()%50)-25.0f)*10.0f;
     31         }
     32         break;
     33     case Qt::Key_8:                                     //按下8增加y方向加速度
     34         for (int i=0; i<MAX_PARTICLES; i++)
     35         {
     36             if (m_Particles[i].yg < 3.0f)
     37             {
     38                 m_Particles[i].yg += 0.05f;
     39             }
     40         }
     41         break;
     42     case Qt::Key_2:                                     //按下2减少y方向加速度
     43         for (int i=0; i<MAX_PARTICLES; i++)
     44         {
     45             if (m_Particles[i].yg > -3.0f)
     46             {
     47                 m_Particles[i].yg -= 0.05f;
     48             }
     49         }
     50         break;
     51     case Qt::Key_6:                                     //按下6增加x方向加速度
     52         for (int i=0; i<MAX_PARTICLES; i++)
     53         {
     54             if (m_Particles[i].xg < 3.0f)
     55             {
     56                 m_Particles[i].xg += 0.05f;
     57             }
     58         }
     59         break;
     60     case Qt::Key_4:                                     //按下4减少x方向加速度
     61         for (int i=0; i<MAX_PARTICLES; i++)
     62         {
     63             if (m_Particles[i].xg > -3.0f)
     64             {
     65                 m_Particles[i].xg -= 0.05f;
     66             }
     67         }
     68         break;
     69     case Qt::Key_Plus:                                  //+ 号按下加速粒子
     70         if (m_Slowdown > 1.0f)
     71         {
     72             m_Slowdown -= 0.05f;
     73         }
     74         break;
     75     case Qt::Key_Minus:                                 //- 号按下减速粒子
     76         if (m_Slowdown < 3.0f)
     77         {
     78             m_Slowdown += 0.05f;
     79         }
     80         break;
     81     case Qt::Key_PageUp:                                //PageUp按下使粒子靠近屏幕
     82         m_Deep += 0.5f;
     83         break;
     84     case Qt::Key_PageDown:                              //PageDown按下使粒子远离屏幕
     85         m_Deep -= 0.5f;
     86         break;
     87     case Qt::Key_Return:                                //回车键为是否彩虹模式的切换键
     88         m_Rainbow = !m_Rainbow;
     89         break;
     90     case Qt::Key_Space:                                 //空格键为颜色切换键
     91         m_Rainbow = false;
     92         m_Color++;
     93         if (m_Color > 11)
     94         {
     95             m_Color = 0;
     96         }
     97         break;
     98     case Qt::Key_Up:                                    //Up按下增加粒子y轴正方向的速度
     99         if (m_ySpeed < 400.0f)
    100         {
    101             m_ySpeed += 5.0f;
    102         }
    103         break;
    104     case Qt::Key_Down:                                  //Down按下减少粒子y轴正方向的速度
    105         if (m_ySpeed > -400.0f)
    106         {
    107             m_ySpeed -= 5.0f;
    108         }
    109         break;
    110     case Qt::Key_Right:                                 //Right按下增加粒子x轴正方向的速度
    111         if (m_xSpeed < 400.0f)
    112         {
    113             m_xSpeed += 5.0f;
    114         }
    115         break;
    116     case Qt::Key_Left:                                  //Left按下减少粒子x轴正方向的速度
    117         if (m_xSpeed > -400.0f)
    118         {
    119             m_xSpeed -= 5.0f;
    120         }
    121         break;
    122     }
    123 }

    我感觉注释已经写得比较清楚了,就不解释太多了,具体里面的值是怎么得到的,其实就是一点点尝试,感觉效果好久用了,就这么简单!大家注意一下Tab键按下后,全部粒子会回到原点,重新从原点出发,并且我们给它们重新生成速度,方式和初始化时是相同的,这样就又产生了爆炸效果。

    现在就可以运行程序查看效果了!

  • 相关阅读:
    core dump的使用
    wav文件格式
    Unicode编码 【转】
    WAV格式中常见的压缩编码
    两台交换机级联端口mac地址表维护(转载)
    pthread_cond_wait()的使用方法
    makefile自动依赖[转]
    11月的第一天
    再读simpledb 之 事务管理的实现(3)
    再读simpledb 之 元数据管理(1)
  • 原文地址:https://www.cnblogs.com/ybqjymy/p/14048526.html
Copyright © 2020-2023  润新知