基于OpenGL编写一个简易的2D渲染框架-06 编写一个粒子系统

摘要:
在这篇文章中,我将详细说明如何编写一个简易的粒子系统。粒子系统可以模拟许多效果,下图便是这次的粒子系统的显示效果。这个对象根据粒子所受的力来驱使粒子移动及使粒子的属性变化。对应就上面的第二点,也是整个粒子系统的核心部分。ParticleDescription:粒子描述,拥有创建粒子系统的数据。除了渲染数据,还包括驱使粒子移动运动的数据,在ParticleEffect对象中进行计算。发射器存在一个发射总时间,过了这个时间就停止发射粒子。

在这篇文章中,我将详细说明如何编写一个简易的粒子系统。

粒子系统可以模拟许多效果,下图便是这次的粒子系统的显示效果。为了方便演示,就弄成了一个动图。

基于OpenGL编写一个简易的2D渲染框架-06 编写一个粒子系统第1张

图中,同时显示了 7 种不同粒子效果,看上去效果挺炫酷的。

粒子编辑器

使用粒子编辑器,可以在可视化视图中快速、简便的做出想要的粒子效果。这个粒子系统支持导入 cocos2d 粒子编辑器文件,而且粒子系统的也是围绕这个编辑器来设计的

基于OpenGL编写一个简易的2D渲染框架-06 编写一个粒子系统第2张

在我看来,要编写一个粒子系统,主要解决两个问题:

1、粒子系统的工作流程(粒子系统是如何工作的)

2、如何实现大量粒子运动及属性变化的多样性

先说第二点,其实粒子就是在力的作用下移动。由于所有粒子所受到的力不一样,所以粒子的运动存在多样性。其核心就是随机函数,用随机函数很容易实现大量粒子运动及属性变化的多样性。只需要在初始化粒子的时候,给予粒子不同的力以及不同的属性变化。其也体现在粒子编辑器中,在粒子编辑器中,几乎每个属性都对应着一个可变范围的值。

粒子系统的结构

我将粒子系统分为几个部分

基于OpenGL编写一个简易的2D渲染框架-06 编写一个粒子系统第3张

ParticleSystem:粒子系统对象

ParticleEmitter:粒子发射器,用于发射粒子,所有粒子都必须由这个对象来创建

ParticleEffect:粒子效果,上面说过粒子是在力的作用下运动的。这个对象根据粒子所受的力来驱使粒子移动及使粒子的属性变化。对应就上面的第二点,也是整个粒子系统的核心部分。

ParticleMemory:粒子内存池,由于粒子系统伴随着大量粒子的创建和移除。所以一开始就创建一定数量的粒子,需要(创建)粒子的时候返回粒子索引即可。

ParticleDescription:粒子描述,拥有创建粒子系统的数据。也就是用来初始化 ParticleSystem 和 ParticleEmitter 对象的结构数据。

Particle:粒子对象,拥有粒子属性数据,可以通过渲染器渲染粒子显示出来。其结构如下

    structParticle
    {
        Vec2 vPos;
        Vec2 vChangePos;
        Vec2 vStartPos;

        Color cColor;
        Color cDeltaColor;

        floatfCurrentSize;
        floatfSize;
        floatfDeltaSize;

        floatfRotation;
        floatfDeltaRotation;

        floatfRemainingLife;

        /*重力模式数据 */
        structGravityModeData
        {
            Vec2  vInitialValocity;        /*初速度 */
            float fRadialAccel;            /*径向加速度(法相加速度), 与运动方向垂直 */
            float fTangentialAccel;        /*切向加速度 */
        } gravityMode;

        /*半径模式数据 */
        structRadiusModeData
        {
            float fAngle;                 /*发射角度 */
            float fDegressPerSecond;      /*每秒旋转角度 */
            float fRadius;                 /*半径 */
            float fDelatRadius;            /*半径变化量 */
        } radiusMode;
    };

包含渲染所需要的位置坐标、颜色、大小和旋转角度的属性数据。除了渲染数据,还包括驱使粒子移动运动的数据,在 ParticleEffect 对象中进行计算。

如果粒子在重力模式下运动,则需要重力、初速度、切向加速度和径向加速度。

如果粒子在半径模式下运动,粒子就不是在粒子的作用下运动了,而是通过半径大小和每秒旋转角度计算运动轨迹。

上述数据都与粒子编辑器左侧属性面板对应。

粒子系统工作流程

1、通过 ParticleDescription 创建粒子系统 ParticleSystem,然后初始化 ParticleEmitter 和 ParticleEffect

2、ParticleEmitter 不断发射(创建)粒子

3、所有粒子都在 ParticleEffect 中更新属性,并移除已经消亡的粒子

4、渲染所有粒子

粒子池 ParticleMemory

由于粒子系统有大量粒子的生成和销毁,所以事先创建一定数量的粒子(也是粒子系统支持的最大数量的粒子)到容器中,需要生成粒子时就从容器中取出,销毁粒子则重新将粒子储存到容器中。

        static std::vector<Particle*>vParticlePool;      // 存储粒子的容器
        static std::vector<Particle*> vUnusedParticleList;   // 存储未被使用的粒子容器

事先创建大量粒子,保存到粒子池中

        Particle* particle =nullptr;
        for ( int i = 0; i < size; i++) {
            particle = newParticle;
            vParticlePool.push_back(particle);
            vUnusedParticleList.push_back(particle);
        }

需要生成粒子时,从存储着未被使用的粒子的粒子容器 vUnusedParticleList 中取出粒子

    Particle*ParticleMemory::allocParticle()
    {
        if ( (nFreeIndex >= vParticlePool.size() - 1) ) {
            returnnullptr;
        }
        else{
            return vUnusedParticleList[nFreeIndex++];
        }
    }

销毁粒子时

    void ParticleMemory::freeParticle(Particle*particle)
    {
        assert(nFreeIndex != 0);
        vUnusedParticleList[--nFreeIndex] =particle;
    }

粒子池的实现较为简单

粒子发射器 ParticleEmitter

发射器只主要做的工作就是生成粒子(不负责销毁生命已经结束的粒子),给发射器添加一个属性——发射速率 emitRate(每秒发射粒子的数量),发射器就按照发射速率来发射粒子。

    void ParticleEmitter::emitParticles(floatdt)
    {
        /*发射一个粒子所用时间 */
        float emit_particle_time = 1 /emitRate;

        /*累计发射时间 */
        if ( vParticleList.size() <particleCount ) {
            fEmitCounter +=dt;
        }

        /*在时间 emit_counter 发射 emit_counter / rate 个粒子 */
        while ( vParticleList.size() < particleCount && fEmitCounter > 0) {
            this->addParticle();
            fEmitCounter -=emit_particle_time;
        }

        fElapsed +=dt;
        if ( duration != -1 && duration <fElapsed ) {
            fElapsed = 0;
            this->stopEmitting();
        }
    }

这个函数每一帧都被调用,dt 就是两帧间的时间间隔,也是发射器在上次发射粒子后到现在被调用经过的时间。接着通过发射速率计算发射一个粒子所需的时间,最后计算出这个发射粒子的数量 = 发射总时间 ÷ 发射一个粒子时间。发射器存在一个发射总时间,过了这个时间就停止发射粒子。

生成粒子实现如下,每个发射器都有粒子数量限制,不能发射多于这个数量的粒子。从粒子池中取出一个粒子(如果粒子池中的粒子都被使用了,就返回 nullptr),添加到发射器的粒子列表。然后使用 ParticleEffect 初始化粒子

    voidParticleEmitter::addParticle()
    {
        if ( vParticleList.size() == particleCount ) return;

        Particle* particle =ParticleMemory::allocParticle();
        if ( particle == nullptr ) return;

        /*存储粒子并初始化粒子 */vParticleList.push_back(particle);
        pParticleEffect->initParticle(this, particle);
    }

粒子影响 ParticleEffect

前面的就是生成一定数量的粒子,那么如何让粒子动起来并且每个粒子的运动轨迹和属性变化不同呢?生成的粒子在 ParticleEffect 对象中显示运动和属性变化。

首先,新的粒子要进行初始化

    void ParticleEffect::initParticle(ParticleEmitter* pe, Particle*particle)
    {
        /*粒子起始位置 */particle->vPos.x = pe->getEmitPos().x + pe->getEmitPosVar().x *RANDOM_MINUS1_1();
        particle->vPos.y = pe->getEmitPos().y + pe->getEmitPosVar().y *RANDOM_MINUS1_1();

        particle->vStartPos = pe->getEmitPos();
        particle->vChangePos = particle->vPos;

        /*粒子生命 */particle->fRemainingLife = MAX(0.1, life + lifeVar *RANDOM_MINUS1_1());

        /*粒子的颜色变化值 */Color begin_color, end_color;
        begin_color.r = CLAMPF(beginColor.r + beginColorVar.r * RANDOM_MINUS1_1(), 0, 1);
        begin_color.g = CLAMPF(beginColor.g + beginColorVar.g * RANDOM_MINUS1_1(), 0, 1);
        begin_color.b = CLAMPF(beginColor.b + beginColorVar.b * RANDOM_MINUS1_1(), 0, 1);
        begin_color.a = CLAMPF(beginColor.a + beginColorVar.a * RANDOM_MINUS1_1(), 0, 1);

        end_color.r = CLAMPF(endColor.r + endColorVar.r * RANDOM_MINUS1_1(), 0, 1);
        end_color.g = CLAMPF(endColor.g + endColorVar.g * RANDOM_MINUS1_1(), 0, 1);
        end_color.b = CLAMPF(endColor.b + endColorVar.b * RANDOM_MINUS1_1(), 0, 1);
        end_color.a = CLAMPF(endColor.a + endColorVar.a * RANDOM_MINUS1_1(), 0, 1);

        float tmp = 1 / (particle->fRemainingLife);
        particle->cColor =begin_color;
        particle->cDeltaColor.r = (end_color.r - begin_color.r) *tmp;
        particle->cDeltaColor.g = (end_color.g - begin_color.g) *tmp;
        particle->cDeltaColor.b = (end_color.b - begin_color.b) *tmp;
        particle->cDeltaColor.a = (end_color.a - begin_color.a) *tmp;

        /*粒子大小 */
        float begin_size = MAX(0, beginSize + beginSizeVar *RANDOM_MINUS1_1());
        float end_size = MAX(0, endSize + endSizeVar *RANDOM_MINUS1_1());

        particle->fSize =begin_size;
        particle->fDeltaSize = (end_size - begin_size) / particle->fRemainingLife;

        /*粒子旋转角度 */
        float begin_spin = toRadian(MAX(0, beginSpin + beginSpinVar *RANDOM_MINUS1_1()));
        float end_spin = toRadian(MAX(0, endSpin + endSpinVar *RANDOM_MINUS1_1()));

        particle->fRotation =begin_spin;
        particle->fDeltaRotation = (end_spin - begin_spin) / particle->fRemainingLife;
    }

发射器会决定粒子的起始位置坐标,在初始化函数中,设置了粒子的生命周期、起始颜色、结束颜色、起始大小、结束大小、起始旋转角度和结束旋转角度。由于使用了随机函数,在给粒子赋值时会随机生成一定范围内的值,所以每个粒子在通过这个函数初始化属性值时都不相同。接下来计算粒子的属性变化时,每个粒子的变化都不相同。以上属性的给定值由编辑器而来。

粒子运动有两种模式,重力模式(动图两侧的粒子效果)和半径模式(动图中间的两个粒子效果),其对粒子的初始化不相同。在类图中,我使用了两个子类GravityParticleEffect 和 RadiusParticleEffect,都继承于 ParticleEffect。

重力模式下粒子还需要初始化的属性值

    void GravityParticleEffect::initParticle(ParticleEmitter* pe, Particle*particle)
    {
        ParticleEffect::initParticle(pe, particle);

        /*计算粒子受到发射器给的初速度大小 */
        float particleSpeed = pe->getEmitSpeed() + pe->getEmitSpeedVar() *RANDOM_MINUS1_1();

        /*计算粒子初速度的方向,即发射器发射粒子的发射方向 */
        float angle = pe->getEmitAngle() + pe->getEmitAngleVar() *RANDOM_MINUS1_1();
        Vec2 particleDirection =Vec2(cosf(toRadian(angle)), sinf(toRadian(angle)));
        
        /*设置粒子的起始加速度(包括大小及方向)*/particle->gravityMode.vInitialVelocity = particleDirection *particleSpeed;

        /*粒子切向加速度、径向加速度 */particle->gravityMode.fTangentialAccel = gravityMode.fTangentialAccel + gravityMode.fTangentialAccelVar *RANDOM_MINUS1_1();
        particle->gravityMode.fRadialAccel = gravityMode.fRadialAccel + gravityMode.fRadialAccelVar *RANDOM_MINUS1_1();
    }

在 update 函数中,会计算粒子的运动轨迹及属性变化

    void GravityParticleEffect::update(ParticleEmitter* pe, floatdt)
    {
        std::list<Particle*>* indexList = pe->getParticleList();

        for ( auto it = indexList->begin(); it != indexList->end(); ) {
            Particle* p = (*it);

            p->fRemainingLife -=dt;

            if ( p->fRemainingLife > 0) {
                staticVec2 offset, radial, tangential;

                /*径向加速度 */
                if ( p->vChangePos.x || p->vChangePos.y ) {
                    offset = p->gravityMode.vInitialVelocity;
                    radial =offset.normalize();
                }
                tangential =radial;
                radial = radial * p->gravityMode.fRadialAccel;

                /*切向加速度 */
                float newy =tangential.x;
                tangential.x = -tangential.y;
                tangential.y =newy;
                tangential = tangential * p->gravityMode.fTangentialAccel;

                /*合力 */offset = (radial + tangential + gravityMode.vGravity) *dt;

                /*在合力作用下移动粒子 */p->gravityMode.vInitialVelocity = p->gravityMode.vInitialVelocity +offset;
                p->vChangePos = p->vChangePos + p->gravityMode.vInitialVelocity *dt;

                /*属性变化 */p->cColor = p->cColor + p->cDeltaColor *dt;
                p->fSize = MAX(0, p->fSize + p->fDeltaSize *dt);
                p->fRotation = p->fRotation + p->fDeltaRotation *dt;

                if ( motionMode ==MotionMode::MOTION_MODE_RELATIVE ) {
                    Vec2 diff = pe->getEmitPos() - p->vStartPos;
                    p->vPos = p->vChangePos +diff;
                }
                else{
                    p->vPos = p->vChangePos;
                }
                ++it;
            }
            else{
                /*移除结束生命周期的粒子 */ParticleMemory::freeParticle(*it);
                it = indexList->erase(it);
            }
        }
    }

获取发射器中的所有粒子,遍历所有粒子设置其属性值。

粒子默认受到三个力的作用,分别是重力、方向和粒子初速度方向相同的力,向心力(方向与粒子初速度方向垂直),(在我理解中,切线加速度方向和粒子运动方向相同,径向加速度方向和运动速度垂直。但按照这样方向计算粒子受到的力时,就和粒子编辑器实现的粒子效果不一样了。我也不清楚这个粒子编辑器中这两个加速度的方向是怎么样的,我猜测在粒子编辑器中,径向加速度和粒子初速度方向相同,而切向加速度和粒子的运动方向垂直)。

先分别计算这三个的大小和方向,再计算粒子受到的合力(把三个力相加),这个合力会改变粒子的初速度,接着使粒子在初速度 InitialVelocity 下移动。以上就是简单的矢量运算,所以粒子就在力的作用下运动了。

如果仔细观测动图左侧的两团移动的火焰的话,会发现它们有些不同。上面那团火焰,所有粒子的运动会跟随发射器移动而整体运动(即发射出去的粒子除了在力的作用下运动时,还会跟随发射器的位置偏移而偏移),如果火焰是蜡烛上的火焰,类比到现实中就是移动眼睛看到的火焰效果。而下面的火焰,发射出去的粒子只在力的作用下运动,不随发射器位置的改变而改变,类比到现实中就是移动蜡烛后看到的火焰效果。在 ParticleEffect 中,有这样一个属性——运动模式,对应着上面两种情况

MotionMode motionMode;
    /*粒子运动模式 */
    enum classMotionMode
    {
        MOTION_MODE_FREE,        /*粒子运动和发射器无关 */MOTION_MODE_RELATIVE     /*粒子运动跟随发射器位置 */};

在初始化粒子的属性时,记录了发射器发射粒子时的位置坐标

        particle->vStartPos = pe->getEmitPos();
        particle->vChangePos = particle->vPos;

这是因为在这两种模式下粒子位置坐标的计算是不同的,如果运动模式为 MOTION_MODE_RELATIVE,粒子在力作用下的位置坐标还要加上发射器的偏移。否则不用理会,以上是重力模式粒子运动轨迹的计算。

半径模式下粒子还需要初始化的属性值

    void RadialParticleEffect::initParticle(ParticleEmitter* pe, Particle*particle)
    {
        ParticleEffect::initParticle(pe, particle);

        float begin_radius = radiusMode.fBeginRadius + radiusMode.fBeginRadiusVar *RANDOM_MINUS1_1();
        float end_radius = radiusMode.fEndRadius + radiusMode.fEndRadiusVar *RANDOM_MINUS1_1();
        
        particle->radiusMode.fRadius =begin_radius;
        particle->radiusMode.fDelatRadius = (end_radius - begin_radius) / particle->fRemainingLife;

        float degress = pe->getEmitAngle() + pe->getEmitAngleVar() *RANDOM_MINUS1_1();
        particle->radiusMode.fAngle =toRadian(degress);

        degress = radiusMode.fSpinPerSecond + radiusMode.fSpinPerSecondVar *RANDOM_MINUS1_1();
        particle->radiusMode.fDegressPerSecond =toRadian(degress);
    }

设置粒子的起始半径、结束半径和每秒转动的角度,这些时半径模式下计算粒子运动轨迹需要的数据

    void RadialParticleEffect::update(ParticleEmitter* pe, floatdt)
    {
        std::list<Particle*>* indexList = pe->getParticleList();

        for ( auto it = indexList->begin(); it != indexList->end(); ) {
            Particle* p = (*it);

            p->fRemainingLife -=dt;

            if ( p->fRemainingLife > 0) {
                p->radiusMode.fAngle += p->radiusMode.fDegressPerSecond *dt;
                p->radiusMode.fRadius += p->radiusMode.fDelatRadius *dt;

                p->vChangePos.x = cosf(p->radiusMode.fAngle) * p->radiusMode.fRadius;
                p->vChangePos.y = sinf(p->radiusMode.fAngle) * p->radiusMode.fRadius;
                
                if ( motionMode ==MotionMode::MOTION_MODE_FREE ) {
                    p->vPos = p->vChangePos + pe->getEmitPos();
                }
                else{
                    p->vPos = p->vChangePos + p->vStartPos;
                }

                /*属性变化 */p->cColor = p->cColor + p->cDeltaColor *dt;
                p->fSize = MAX(0, p->fSize + p->fDeltaSize *dt);
                p->fRotation = p->fRotation + p->fDeltaRotation *dt;
                ++it;
            }
            else{
                /*移除结束生命周期的粒子 */ParticleMemory::freeParticle(*it);
                it = indexList->erase(it);
            }
        }
    }

这个的计算比较简单,就是通过半径大小和角度值计算粒子的位置坐标,再变换到发射器的位置坐标附近。最后就是粒子属性的变化。

粒子系统 ParticleSystem

创建一个粒子系统所需的属性数据来自粒子描述 ParticleDescription

#pragma once#include "../Math.h"

namespaceSimple2D
{
    /*发射器类型 */
    enum classEmitterType
    {
        EMITTER_TYPE_GRAVITY,     /*重力模式 */EMITTER_TYPE_RADIUS        /*半径模式 */};

    /*粒子运动模式 */
    enum classMotionMode
    {
        MOTION_MODE_FREE,        /*粒子运动和发射器无关 */MOTION_MODE_RELATIVE     /*粒子运动跟随发射器位置 */};

    /*重力模式 */
    structGravityMode
    {
        Vec2 vGravity;                 /*重力方向 */

        float fTangentialAccel;        /*切向加速度 */
        float fTangentialAccelVar;     /*径向加速度变化值 */

        float fRadialAccel;            /*径向加速度 */
        float fRadialAccelVar;         /*径向加速度变化值 */};

    /*半径模式 */
    structRadiusMode
    {
        float fBeginRadius;           /*起始半径  */
        float fBeginRadiusVar;        /*起始半径变化值 */

        float fEndRadius;           /*结束半径 */
        float fEndRadiusVar;        /*结束半径变化值 */

        float fSpinPerSecond;       /*每秒旋转角度 */
        float fSpinPerSecondVar;    /*每秒旋转角度变化值 */};


    classDLL_export ParticleDescription
    {
    public:
        ParticleDescription()
            : vEmitPos(0, 0)
            , vEmitPosVar(0, 0)
            , fEmitAngle(0)
            , fEmitAngleVar(0)
            , fEmitSpeed(0)
            , fEmitSpeedVar(0)
            , nParticleCount(0)
            , fEmitRate(0)
            , fDuration(-1)
            , emitterType(EmitterType::EMITTER_TYPE_GRAVITY)
            , motionMode(MotionMode::MOTION_MODE_FREE)
            , fLife(0)
            , fLifeVar(0)
            , cBeginColor(0, 0, 0, 0)
            , cBeginColorVar(0, 0, 0, 0)
            , cEndColor(0, 0, 0, 0)
            , cEndColorVar(0, 0, 0, 0)
            , fBeginSize(0)
            , fBeginSizeVar(0)
            , fEndSize(0)
            , fEndSizeVar(0)
            , fBeginSpin(0)
            , fBeginSpinVar(0)
            , fEndSpin(0)
            , fEndSpinVar(0)
        {
            gravityMode.fRadialAccel = 0;
            gravityMode.fRadialAccelVar = 0;
            gravityMode.fTangentialAccel = 0;
            gravityMode.fTangentialAccelVar = 0;
            gravityMode.vGravity.set(0, 0);

            radiusMode.fBeginRadius = 0;
            radiusMode.fBeginRadiusVar = 0;
            radiusMode.fEndRadius = 0;
            radiusMode.fEndRadiusVar = 0;
            radiusMode.fSpinPerSecond = 0;
            radiusMode.fSpinPerSecondVar = 0;
        }

        /*发射器属性 */
        Vec2 vEmitPos;                /*发射器位置 */Vec2 vEmitPosVar;            

        float fEmitAngle;            /*发射器发射粒子角度 */
        floatfEmitAngleVar;
                
        float fEmitSpeed;            /*发射器发射粒子速度 */
        floatfEmitSpeedVar;

        int nParticleCount;         /*粒子数量 */
        float fEmitRate;            /*粒子每秒发射速率 */
        float fDuration;            /*发射器发射粒子时间 */
        EmitterType emitterType;
        MotionMode  motionMode;

        /*粒子属性 */

        /*粒子生命周期 */
        floatfLife;
        floatfLifeVar;

        /*粒子的颜色变化 */Color cBeginColor;
        Color cBeginColorVar;
        Color cEndColor;
        Color cEndColorVar;

        /*粒子的大小变化 */
        floatfBeginSize;
        floatfBeginSizeVar;
        floatfEndSize;
        floatfEndSizeVar;

        /*粒子旋转角度变化 */
        floatfBeginSpin;
        floatfBeginSpinVar;
        floatfEndSpin;
        floatfEndSpinVar;

        GravityMode gravityMode;
        RadiusMode radiusMode;
    };
}

通过这个数据对象来初始化 ParticleEmitter 和 ParticleEffect,但你创建一个粒子系统时,只需要设置这个数据对象的参数,然后把它设置到粒子系统中

    void ParticleSystem::setDescription(const ParticleDescription&desc)
    {
        pEmitter->setDecription(desc);
    }
    void ParticleEmitter::setDecription(const ParticleDescription&desc)
    {
        /*发射器属性 */emitPos =desc.vEmitPos;
        emitPosVar =desc.vEmitPosVar;

        emitAngle =desc.fEmitAngle;
        emitAngleVar =desc.fEmitAngleVar;

        emitSpeed =desc.fEmitSpeed;
        emitSpeedVar =desc.fEmitSpeedVar;

        emitRate =desc.fEmitRate;
        duration =desc.fDuration;
        particleCount =desc.nParticleCount;

        /*创建粒子 effect */ParticleEffect* effect =nullptr;
        if ( desc.emitterType ==EmitterType::EMITTER_TYPE_GRAVITY ) {
            effect = newGravityParticleEffect();
        }
        else{
            effect = newRadialParticleEffect();
        }

        effect->setDecription(desc);
        this->setParticleEffect(effect);
    }
    void ParticleEffect::setDecription(const ParticleDescription&desc)
    {
        life =desc.fLife;
        lifeVar =desc.fLifeVar;

        beginColor =desc.cBeginColor;
        beginColorVar =desc.cBeginColorVar;
        endColor =desc.cEndColor;
        endColorVar =desc.cEndColorVar;

        beginSize =desc.fBeginSize;
        beginSizeVar =desc.fBeginSizeVar;
        endSize =desc.fEndSize;
        endSizeVar =desc.fEndSizeVar;

        beginSpin =desc.fBeginSpin;
        beginSpinVar =desc.fBeginSpinVar;
        endSpin =desc.fEndSpin;
        endSpinVar =desc.fEndSpinVar;

        motionMode =desc.motionMode;

        gravityMode =desc.gravityMode;
        radiusMode =desc.radiusMode;
    }

即可完成整个粒子系统的初始化,如果手动设置参数的话,难以调出理想的粒子效果。这时就需要粒子编辑器这个可视化工具了,在编辑器模拟出粒子效果后,导出 plist 文件,通过解析这个文件来设置 ParticleDescription 数据对象。

解析 plist 文件使用 xml 解析器即可,项目中用了 tinyxml 这个库

    ParticleSystem::ParticleConfigMap ParticleSystem::parseParticlePlistFile(const std::string&filename)
    {
        ParticleConfigMap particleConfigMap;

        tinyxml2::XMLDocument doc;
        doc.LoadFile(filename.c_str());

        tinyxml2::XMLElement* root =doc.RootElement();
        tinyxml2::XMLNode* dict = root->FirstChildElement("dict");
        tinyxml2::XMLElement* ele = dict->FirstChildElement();

        std::stringtmpstr1, tmpstr2;
        while( ele ) {
            if ( ele->GetText() != nullptr && strcmp("textureImageData", ele->GetText()) == 0) {
                ele = ele->NextSiblingElement()->NextSiblingElement();
            }
            else{
                tmpstr1 = ele->GetText();
                ele = ele->NextSiblingElement();
                tmpstr2 = ele->GetText() == nullptr ? "0" : ele->GetText();
                ele = ele->NextSiblingElement();

                particleConfigMap.insert(std::make_pair(tmpstr1, tmpstr2));
            }
        }
        returnparticleConfigMap;
    }

将解析后的数据保存到一个映射表中

typedef std::map<std::string, std::string> ParticleConfigMap;

再通过这个表设置 ParticleDescription 参数

    ParticleDescription ParticleSystem::createParticleDescription(ParticleConfigMap&map)
    {
        ParticleDescription desc;

        //================================== 发射器属性 ========================================

        /*发射器角度 */desc.fEmitAngle       = GET_I(map, "angle");
        desc.fEmitAngleVar = GET_I(map, "angleVariance");

        /*发射器速度 */desc.fEmitSpeed       = GET_I(map, "speed");
        desc.fEmitSpeedVar = GET_I(map, "speedVariance");

        //发射器持续时间
        desc.fDuration     = GET_F(map, "duration");

        //发射器模式(重力、径向)
        if ( GET_I(map, "emitterType") ) {
            desc.emitterType =EmitterType::EMITTER_TYPE_RADIUS;
        }
        else{
            desc.emitterType =EmitterType::EMITTER_TYPE_GRAVITY;
        }

        /*最大粒子数量 */desc.nParticleCount = GET_F(map, "maxParticles");

        /*发射区坐标 */desc.vEmitPos.set(GET_F(map, "sourcePositionx"), GET_F(map, "sourcePositiony"));
        desc.vEmitPosVar.set(GET_F(map, "sourcePositionVariancex"), GET_F(map, "sourcePositionVariancey"));

        /*粒子生命周期 */desc.fLife    = GET_F(map, "particleLifespan");
        desc.fLifeVar = GET_F(map, "particleLifespanVariance");

        /*发射速率 */desc.fEmitRate = desc.nParticleCount /desc.fLife;

        //================================== 粒子属性 ========================================

        /*粒子起始颜色 */desc.cBeginColor.set(
            GET_F(map, "startColorRed"), 
            GET_F(map, "startColorGreen"), 
            GET_F(map, "startColorBlue"), 
            GET_F(map, "startColorAlpha"));

        desc.cBeginColorVar.set(
            GET_F(map, "startColorVarianceRed"),
            GET_F(map, "startColorVarianceGreen"),
            GET_F(map, "startColorVarianceBlue"),
            GET_F(map, "startColorVarianceAlpha"));

        /*粒子结束颜色 */desc.cEndColor.set(
            GET_F(map, "finishColorRed"),
            GET_F(map, "finishColorGreen"),
            GET_F(map, "finishColorBlue"),
            GET_F(map, "finishColorAlpha"));

        desc.cEndColorVar.set(
            GET_F(map, "finishColorVarianceRed"),
            GET_F(map, "finishColorVarianceGreen"),
            GET_F(map, "finishColorVarianceBlue"),
            GET_F(map, "finishColorVarianceAlpha"));

        /*粒子大小 */desc.fBeginSize        = GET_F(map, "startParticleSize");
        desc.fBeginSizeVar    = GET_F(map, "startParticleSizeVariance");
        desc.fEndSize        = GET_F(map, "finishParticleSize");
        desc.fEndSizeVar    = GET_F(map, "finishParticleSizeVariance");
                                    
        /*粒子旋转 */desc.fBeginSpin        = GET_F(map, "rotationStart");
        desc.fBeginSpinVar    = GET_F(map, "rotationStartVariance");
        desc.fEndSpin        = GET_F(map, "rotationEnd");
        desc.fEndSpinVar    = GET_F(map, "rotationEndVariance");

        /*粒子运动模式 */MotionMode motionModes[2] ={
            MotionMode::MOTION_MODE_FREE,
            MotionMode::MOTION_MODE_RELATIVE
        };

        desc.motionMode = motionModes[GET_I(map, "positionType")];

        /*GravityMode 重力模式 */desc.gravityMode.vGravity.set(GET_F(map, "gravityx"), GET_F(map, "gravityy"));

        desc.gravityMode.fRadialAccel     = GET_F(map, "radialAcceleration");
        desc.gravityMode.fRadialAccelVar = GET_F(map, "radialAccelVariance");

        desc.gravityMode.fTangentialAccel     = GET_F(map, "tangentialAcceleration");
        desc.gravityMode.fTangentialAccelVar = GET_F(map, "tangentialAccelVariance");

        //RadiusMode 半径模式
        desc.radiusMode.fEndRadius = atof((map)["minRadius"].c_str());
        desc.radiusMode.fEndRadiusVar = atof((map)["minRadiusVariance"].c_str());

        desc.radiusMode.fBeginRadius = atof((map)["maxRadius"].c_str());
        desc.radiusMode.fBeginRadiusVar = atof((map)["maxRadiusVariance"].c_str());

        desc.radiusMode.fSpinPerSecond = atof((map)["rotatePerSecond"].c_str());
        desc.radiusMode.fSpinPerSecondVar = atof((map)["rotatePerSecondVariance"].c_str());

        returndesc;
    }
#define GET_F(map, name) atof((map)[name].c_str())
#define GET_I(map, name) atoi((map)[name].c_str())

渲染粒子

这部分比较简单

    void ParticleSystem::render(Renderer*renderer)
    {
        int begin_index = 0;
        float s = 0, c = 0, x = 0, y = 0;

        auto particleIndex = pEmitter->getParticleList();
        Particle* particle =nullptr;

        int count = particleIndex->size();
        if ( vPositions.size() < count * 4) {
            vPositions.resize(count * 4);
            vColors.resize(count * 4);
        }

        nPositionIndex = 0;
        for ( auto it = particleIndex->begin(); it != particleIndex->end(); ++it ) {
            particle = (*it);

            c = cosf(particle->fRotation) * particle->fSize / 2.0f;
            s = sinf(particle->fRotation) * particle->fSize / 2.0f;

            x = particle->vPos.x;
            y = particle->vPos.y;

            vPositions[nPositionIndex + 0].set(x - c - s, y - c + s, 0);
            vPositions[nPositionIndex + 1].set(x - c + s, y + c + s, 0);
            vPositions[nPositionIndex + 2].set(x + c + s, y + c - s, 0);
            vPositions[nPositionIndex + 3].set(x + c - s, y - c - s, 0);

            vColors[nPositionIndex + 0] = particle->cColor;
            vColors[nPositionIndex + 1] = particle->cColor;
            vColors[nPositionIndex + 2] = particle->cColor;
            vColors[nPositionIndex + 3] = particle->cColor;

            nPositionIndex += 4;
        }

        staticRenderUnit unit;
        unit.pPositions = &vPositions[0];
        unit.nPositionCount =nPositionIndex;
        unit.nIndexCount = nPositionIndex * 1.5;
        unit.color = &vColors[0];
        unit.bSameColor = false;
        unit.texture =texture;
        unit.renderType =RENDER_TYPE_TEXTURE;
        unit.shaderUsage =SU_TEXTURE;
        unit.flag = DEFAULT_INDEX |DEFAULT_TEXCOORD;

        renderer->pushParticleRenderUnit(unit);
    }

要注意的是计算粒子四个顶点的方法,就是坐标点的旋转和坐标变换。

使用粒子系统

使用了一个粒子系统管理器来管理所有粒子系统,就是把所有粒子系统集中在一起 update 和 render

    void ParticleSystemManager::update(floatdt)
    {
        for ( auto&ele : vParticleSystems ) {
            ele->update(dt);
        }
    }

    void ParticleSystemManager::render(Renderer*renderer)
    {
        for ( auto&ele : vParticleSystems ) {
            ele->render(renderer);
        }
    }

最后在主函数中使用粒子系统

ParticleSystemManager particleSystemManager;

    ParticleSystem* fire1PS = newParticleSystem;
    fire1PS->initWithPlist("Particle/fire2.plist");
    fire1PS->setTexture("Particle/fire.png");
    fire1PS->getEmitter()->setEmitPos(Vec2(200, 350));
    fire1PS->getEmitter()->getParticleEffect()->motionMode =MotionMode::MOTION_MODE_RELATIVE;

    ParticleSystem* fire2PS = newParticleSystem;
    fire2PS->initWithPlist("Particle/fire1.plist");
    fire2PS->setTexture("Particle/fire.png");
    fire2PS->getEmitter()->setEmitPos(Vec2(200, 50));
    fire2PS->getEmitter()->getParticleEffect()->motionMode =MotionMode::MOTION_MODE_FREE;

    ParticleSystem* radius1PS = newParticleSystem;
    radius1PS->initWithPlist("Particle/radius1.plist");
    radius1PS->setTexture("Particle/fire.png");
    radius1PS->getEmitter()->setEmitPos(Vec2(400, 420));

    ParticleSystem* radius2PS = newParticleSystem;
    radius2PS->initWithPlist("Particle/radius2.plist");
    radius2PS->setTexture("Particle/fire.png");
    radius2PS->getEmitter()->setEmitPos(Vec2(400, 120));

    ParticleSystem* starPS = newParticleSystem;
    starPS->initWithPlist("Particle/star.plist");
    starPS->setTexture("Particle/star.png");
    starPS->getEmitter()->setEmitPos(Vec2(600, 350));
    starPS->getEmitter()->getParticleEffect()->motionMode =MotionMode::MOTION_MODE_FREE;

    ParticleSystem* testPS = newParticleSystem;
    testPS->initWithPlist("Particle/test.plist");
    testPS->setTexture("Particle/fire.png");
    testPS->getEmitter()->setEmitPos(Vec2(600, 80));
    testPS->getEmitter()->getParticleEffect()->motionMode =MotionMode::MOTION_MODE_RELATIVE;

    ParticleSystem* fallenLeavesPS = newParticleSystem;
    fallenLeavesPS->initWithPlist("Particle/fallenLeaves.plist");
    fallenLeavesPS->setTexture("Particle/fallenLeaves.png");
    fallenLeavesPS->getEmitter()->setEmitPos(Vec2(400, 650));

    particleSystemManager.appendParticleSystem(fire1PS);
    particleSystemManager.appendParticleSystem(fire2PS);
    particleSystemManager.appendParticleSystem(radius1PS);
    particleSystemManager.appendParticleSystem(radius2PS);
    particleSystemManager.appendParticleSystem(starPS);
    particleSystemManager.appendParticleSystem(testPS);
    particleSystemManager.appendParticleSystem(fallenLeavesPS);
            particleSystemManager.update(frame_interval / 1000);
            particleSystemManager.render(graphicsContext.getRenderer());

源码下载:http://pan.baidu.com/s/1skOmP21

免责声明:文章转载自《基于OpenGL编写一个简易的2D渲染框架-06 编写一个粒子系统》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇VSCode插件开发全攻略(四)命令、菜单、快捷键Java编程语言基础 第三章 我行我素换购下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

Simulink仿真入门到精通(五) Simulink模型的仿真

5.1 模型的配置仿真 由各种模块所构建的可视化逻辑连接,只是模型的外在表现,模型仿真的核心驱动器是被称作解算器(Solver)的组件,相当于Simulink仿真过程的心脏,驱动着模型仿真,它在每一个采样时间点更新模型中所有的状态和信号变量,并计算下一步的步长。除此之外,模型还具有一个参数配置集合(Configuration Parameter Set),...

unity3D游戏开发三之unity编辑器二

转:http://blog.csdn.net/kuloveyouwei/article/details/23020995 下面我们介绍下GameObject,游戏对象/物体,通过游戏对象我们可以创建游戏对象,如灯光、粒子、模型、GUI等。 GameObject菜单 通过Create Other,我们可以创建系统自带的一些游戏对象,具体如下: Partic...

C# Random生成相同随机数的解决方案

1.生成任意随机数 Random random = new Random(); random.Next(minvale, maxvale); 时间短重复 2.利用种子生成不重复随机数 (a)生成随机数时:Random ran = new Random((int)DateTime.Now.Ticks); ran .Next(minvale, maxva...

Cocos3d-x 发布第一版

从去年开始11一月,我开始一个又一个人cocos3d的C++改写版本号。现在见效。所有cocos3d的OC代码改写成了C++。 在正常Android和Windows在执行。上周,正式发布了第一个版本。上传GitHub上,喜欢的朋友能够点击链接訪问。 Cocos3d-x基于cocos2d-x 2.x 编写。利用cocos2d的跨平台优势,单独封装了一个3D...

Java编程:根据给定的日期,计算两个日期之间的时间差

计算两个Date之间的时间差,基本思路为把Date转换为ms(微秒),然后计算两个微秒时间差。 时间的兑换规则如下: 1s秒 = 1000ms毫秒 1min分种 = 60s秒 1hours小时 = 60min分钟 1day天 = 24hours小时   package com.qiyadeng.date; import java.text.SimpleDa...

点云下采样2

来源:https://blog.csdn.net/weixin_41281151/article/details/107125844 点云体素降采样(Voxel Filter Downsampling)代码参考网址秦乐乐CSDN博客理论参考知乎博主:WALL-E 1.方法Centroid 均值采样Random select 随机采样 2.伪代码流程 注解...