【Qt OpenGL教程】19:粒子系统

来源:互联网 发布:金盏花洁面啫喱知乎 编辑:程序博客网 时间:2024/05/16 05:26

第19课:粒子系统 (参照NeHe)

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

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


程序运行时效果如下:



下面进入教程:


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

#ifndef MYGLWIDGET_H#define MYGLWIDGET_H#include <QWidget>#include <QGLWidget>class MyGLWidget : public QGLWidget{    Q_OBJECTpublic:    explicit MyGLWidget(QWidget *parent = 0);    ~MyGLWidget();protected:    //对3个纯虚函数的重定义    void initializeGL();    void resizeGL(int w, int h);    void paintGL();    void keyPressEvent(QKeyEvent *event);           //处理键盘按下事件private:    bool fullscreen;                                //是否全屏显示    QString m_FileName;                             //图片的路径及文件名    GLuint m_Texture;                               //储存一个纹理    static const int MAX_PARTICLES = 1000;          //最大粒子数    static const GLfloat COLORS[12][3];             //彩虹的颜色    bool m_Rainbow;                                 //是否为彩虹模式    GLuint m_Color;                                 //当前的颜色    float m_Slowdown;                               //减速粒子    float m_xSpeed;                                 //x方向的速度    float m_ySpeed;                                 //y方向的速度    float m_Deep;                                   //移入屏幕的距离    struct Particle                                 //创建粒子结构体    {        bool active;                                //是否激活        float life;                                 //粒子生命        float fade;                                 //衰减速度        float r, g, b;                              //粒子颜色        float x, y, z;                              //位置坐标        float xi, yi, zi;                           //各方向速度        float xg, yg, zg;                           //各方向加速度    } m_Particles[MAX_PARTICLES];                   //存放1000个粒子的数组};#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,在构造函数中对新增变量进行初始化,具体代码如下:

const GLfloat MyGLWidget::COLORS[][3] =                 //彩虹的颜色{    {1.0f, 0.5f, 0.5f}, {1.0f, 0.75f, 0.5f}, {1.0f, 1.0f, 0.5f},    {0.75f, 1.0f, 0.5f}, {0.5f, 1.0f, 0.5f}, {0.5f, 1.0f, 0.75f},    {0.5f, 1.0f, 1.0f}, {0.5f, 0.75f, 1.0f}, {0.5f, 0.5f, 1.0f},    {0.75f, 0.5f, 1.0f}, {1.0f, 0.5f, 1.0f}, {1.0f, 0.5f, 0.75f}};MyGLWidget::MyGLWidget(QWidget *parent) :    QGLWidget(parent){    fullscreen = false;    m_FileName = "D:/QtOpenGL/QtImage/Particle.bmp";    //应根据实际存放图片的路径进行修改    m_Rainbow = true;    m_Color = 0;    m_Slowdown = 2.0f;    m_xSpeed = 0.0f;    m_ySpeed = 0.0f;    m_Deep = -40.0f;    for (int i=0; i<MAX_PARTICLES; i++)                 //循环初始化所以粒子    {        m_Particles[i].active = true;                   //使所有粒子为激活状态        m_Particles[i].life = 1.0f;                     //所有粒子生命值为最大        //随机生成衰减速率        m_Particles[i].fade = float(rand()%100)/1000.0f+0.001;        //粒子的颜色        m_Particles[i].r = COLORS[int(i*(12.0f/MAX_PARTICLES))][0];        m_Particles[i].g = COLORS[int(i*(12.0f/MAX_PARTICLES))][1];        m_Particles[i].b = COLORS[int(i*(12.0f/MAX_PARTICLES))][2];        //粒子的初始位置        m_Particles[i].x = 0.0f;        m_Particles[i].y = 0.0f;        m_Particles[i].z = 0.0f;        //随机生成x、y、z轴方向速度        m_Particles[i].xi = float((rand()%50)-26.0f)*10.0f;        m_Particles[i].yi = float((rand()%50)-25.0f)*10.0f;        m_Particles[i].zi = float((rand()%50)-25.0f)*10.0f;        m_Particles[i].xg = 0.0f;                       //设置x方向加速度为0        m_Particles[i].yg = -0.8f;                      //设置y方向加速度为-0.8        m_Particles[i].zg = 0.0f;                       //设置z方向加速度为0    }    QTimer *timer = new QTimer(this);                   //创建一个定时器    //将定时器的计时信号与updateGL()绑定    connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));    timer->start(10);                                   //以10ms为一个计时周期}
注意到我们在构造函数之前对定义的静态常量数组COLORS进行初始化,一共包含12种渐变颜色,从红色到紫罗兰。进入构造函数一开始是更换纹理图片以及增加变量的初始化,这些没什么好解释的,下面我们重点看循环部分。我们利用循环来初始化每个粒子,我们让粒子变活跃(不活跃的粒子在屏幕上是不会显示的)之后,我们给它lfie。life满值是1.0f,这也给粒子完整的光亮。值得一提,把粒子的生命衰退和颜色渐暗绑到一起,效果真的很不错!

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


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

void MyGLWidget::initializeGL()                         //此处开始对OpenGL进行所以设置{    m_Texture = bindTexture(QPixmap(m_FileName));       //载入位图并转换成纹理    glEnable(GL_TEXTURE_2D);                            //启用纹理映射    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);               //黑色背景    glShadeModel(GL_SMOOTH);                            //启用阴影平滑    glClearDepth(1.0);                                  //设置深度缓存    glDisable(GL_DEPTH_TEST);                           //禁止深度测试    glEnable(GL_BLEND);                                 //启用融合    glBlendFunc(GL_SRC_ALPHA, GL_ONE);                  //设置融合因子    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);  //告诉系统对透视进行修正    glHint(GL_POINT_SMOOTH_HINT, GL_NICEST);}
我们在中间启用了融合并设置了融合因子,这是为了我们的粒子能有不同颜色。然后我们禁用了深度测试,因为如果启用深度测试的话,纹理之间会出现覆盖现象,那样画面简直一团糟。


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

void MyGLWidget::paintGL()                              //从这里开始进行所以的绘制{    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存    glLoadIdentity();                                   //重置模型观察矩阵    glBindTexture(GL_TEXTURE_2D, m_Texture);    for (int i=0; i<MAX_PARTICLES; i++)                 //循环所以的粒子    {        if (m_Particles[i].active)                      //如果粒子为激活的        {            float x = m_Particles[i].x;                 //x轴位置            float y = m_Particles[i].y;                 //y轴位置            float z = m_Particles[i].z + m_Deep;        //z轴位置            //设置粒子颜色            glColor4f(m_Particles[i].r, m_Particles[i].g,                      m_Particles[i].b, m_Particles[i].life);            glBegin(GL_TRIANGLE_STRIP);                 //绘制三角形带                glTexCoord2d(1, 1);glVertex3f(x+0.5f, y+0.5f, z);                glTexCoord2d(0, 1);glVertex3f(x-0.5f, y+0.5f, z);                glTexCoord2d(1, 0);glVertex3f(x+0.5f, y-0.5f, z);                glTexCoord2d(0, 0);glVertex3f(x-0.5f, y-0.5f, z);            glEnd();            //更新各方向坐标及速度            m_Particles[i].x += m_Particles[i].xi/(m_Slowdown*1000);            m_Particles[i].y += m_Particles[i].yi/(m_Slowdown*1000);            m_Particles[i].z += m_Particles[i].zi/(m_Slowdown*1000);            m_Particles[i].xi += m_Particles[i].xg;            m_Particles[i].yi += m_Particles[i].yg;            m_Particles[i].zi += m_Particles[i].zg;            m_Particles[i].life -= m_Particles[i].fade; //减少粒子的生命值            if (m_Particles[i].life < 0.0f)             //如果粒子生命值小于0            {                m_Particles[i].life = 1.0f;             //产生一个新粒子                m_Particles[i].fade = float(rand()%100)/1000.0f+0.003f;                m_Particles[i].r = colors[m_Color][0];  //设置颜色                m_Particles[i].g = colors[m_Color][1];                m_Particles[i].b = colors[m_Color][2];                m_Particles[i].x = 0.0f;                //粒子出现在屏幕中央                m_Particles[i].y = 0.0f;                m_Particles[i].z = 0.0f;                //随机生成粒子速度                m_Particles[i].xi = m_xSpeed + float((rand()%60)-32.0f);                m_Particles[i].yi = m_ySpeed + float((rand()%60)-30.0f);                m_Particles[i].zi = float((rand()%60)-30.0f);            }        }    }    if (m_Rainbow)                                      //如果为彩虹模式    {        m_Color++;                                      //进行颜色的变换        if (m_Color > 11)        {            m_Color = 0;        }    }}
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,这个控制了粒子大体得移动方向。最后我们给粒子分配当前的颜色就搞定循环了。

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


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

void MyGLWidget::keyPressEvent(QKeyEvent *event){    switch (event->key())    {    case Qt::Key_F1:                                    //F1为全屏和普通屏的切换键        fullscreen = !fullscreen;        if (fullscreen)        {            showFullScreen();        }        else        {            showNormal();        }        updateGL();        break;    case Qt::Key_Escape:                                //ESC为退出键        close();        break;    case Qt::Key_Tab:                                   //Tab按下使粒子回到原点,产生爆炸        for (int i=0; i<MAX_PARTICLES; i++)        {            m_Particles[i].x = 0.0f;            m_Particles[i].y = 0.0f;            m_Particles[i].z = 0.0f;            //随机生成速度            m_Particles[i].xi = float((rand()%50)-26.0f)*10.0f;            m_Particles[i].yi = float((rand()%50)-25.0f)*10.0f;            m_Particles[i].zi = float((rand()%50)-25.0f)*10.0f;        }        break;    case Qt::Key_8:                                     //按下8增加y方向加速度        for (int i=0; i<MAX_PARTICLES; i++)        {            if (m_Particles[i].yg < 3.0f)            {                m_Particles[i].yg += 0.05f;            }        }        break;    case Qt::Key_2:                                     //按下2减少y方向加速度        for (int i=0; i<MAX_PARTICLES; i++)        {            if (m_Particles[i].yg > -3.0f)            {                m_Particles[i].yg -= 0.05f;            }        }        break;    case Qt::Key_6:                                     //按下6增加x方向加速度        for (int i=0; i<MAX_PARTICLES; i++)        {            if (m_Particles[i].xg < 3.0f)            {                m_Particles[i].xg += 0.05f;            }        }        break;    case Qt::Key_4:                                     //按下4减少x方向加速度        for (int i=0; i<MAX_PARTICLES; i++)        {            if (m_Particles[i].xg > -3.0f)            {                m_Particles[i].xg -= 0.05f;            }        }        break;    case Qt::Key_Plus:                                  //+ 号按下加速粒子        if (m_Slowdown > 1.0f)        {            m_Slowdown -= 0.05f;        }        break;    case Qt::Key_Minus:                                 //- 号按下减速粒子        if (m_Slowdown < 3.0f)        {            m_Slowdown += 0.05f;        }        break;    case Qt::Key_PageUp:                                //PageUp按下使粒子靠近屏幕        m_Deep += 0.5f;        break;    case Qt::Key_PageDown:                              //PageDown按下使粒子远离屏幕        m_Deep -= 0.5f;        break;    case Qt::Key_Return:                                //回车键为是否彩虹模式的切换键        m_Rainbow = !m_Rainbow;        break;    case Qt::Key_Space:                                 //空格键为颜色切换键        m_Rainbow = false;        m_Color++;        if (m_Color > 11)        {            m_Color = 0;        }        break;    case Qt::Key_Up:                                    //Up按下增加粒子y轴正方向的速度        if (m_ySpeed < 400.0f)        {            m_ySpeed += 5.0f;        }        break;    case Qt::Key_Down:                                  //Down按下减少粒子y轴正方向的速度        if (m_ySpeed > -400.0f)        {            m_ySpeed -= 5.0f;        }        break;    case Qt::Key_Right:                                 //Right按下增加粒子x轴正方向的速度        if (m_xSpeed < 400.0f)        {            m_xSpeed += 5.0f;        }        break;    case Qt::Key_Left:                                  //Left按下减少粒子x轴正方向的速度        if (m_xSpeed > -400.0f)        {            m_xSpeed -= 5.0f;        }        break;    }}
我感觉注释已经写得比较清楚了,就不解释太多了,具体里面的值是怎么得到的,其实就是一点点尝试,感觉效果好久用了,就这么简单!大家注意一下Tab键按下后,全部粒子会回到原点,重新从原点出发,并且我们给它们重新生成速度,方式和初始化时是相同的,这样就又产生了爆炸效果。

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


全部教程中需要的资源文件点此下载


0 0