一杯清酒邀明月
天下本无事,庸人扰之而烦耳。

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

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

程序运行时效果如下:

下面进入教程:

我们这次将在第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键按下后,全部粒子会回到原点,重新从原点出发,并且我们给它们重新生成速度,方式和初始化时是相同的,这样就又产生了爆炸效果。

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

posted on 2020-11-27 15:59  一杯清酒邀明月  阅读(932)  评论(0编辑  收藏  举报