这次教程中,我们将创建一个简单的粒子系统,并用它来创建一种喷射效果。利用粒子系统,我们可以实现爆炸、喷泉、流星之类的效果,听起来是不是很棒呢!
我们还会讲到一个新东西,三角形带(我的理解就是画很多三角形来组合成我们要的形状),它非常容易使用,而且当需要画很多三角形的时候,它能加快你程序的运行速度。这次教程中,我将教你该如何做一个简单的微粒程序,一旦你了解微粒程序的原理后,再创建例如:火、烟、喷泉等效果将是很轻松的事情。
程序运行时效果如下:
下面进入教程:
我们这次将在第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键按下后,全部粒子会回到原点,重新从原点出发,并且我们给它们重新生成速度,方式和初始化时是相同的,这样就又产生了爆炸效果。
现在就可以运行程序查看效果了!