基於OpenGL編寫一個簡易的2D渲染框架-06 編寫一個粒子系統


在這篇文章中,我將詳細說明如何編寫一個簡易的粒子系統。

粒子系統可以模擬許多效果,下圖便是這次的粒子系統的顯示效果。為了方便演示,就弄成了一個動圖。

圖中,同時顯示了 7 種不同粒子效果,看上去效果挺炫酷的。

 

粒子編輯器

使用粒子編輯器,可以在可視化視圖中快速、簡便的做出想要的粒子效果。這個粒子系統支持導入 cocos2d 粒子編輯器文件,而且粒子系統的也是圍繞這個編輯器來設計的

 

在我看來,要編寫一個粒子系統,主要解決兩個問題:

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

  2、如何實現大量粒子運動及屬性變化的多樣性

 

先說第二點,其實粒子就是在力的作用下移動。由於所有粒子所受到的力不一樣,所以粒子的運動存在多樣性。其核心就是隨機函數,用隨機函數很容易實現大量粒子運動及屬性變化的多樣性。只需要在初始化粒子的時候,給予粒子不同的力以及不同的屬性變化。其也體現在粒子編輯器中,在粒子編輯器中,幾乎每個屬性都對應着一個可變范圍的值。

 

粒子系統的結構

我將粒子系統分為幾個部分

ParticleSystem:粒子系統對象

ParticleEmitter:粒子發射器,用於發射粒子,所有粒子都必須由這個對象來創建

ParticleEffect:粒子效果,上面說過粒子是在力的作用下運動的。這個對象根據粒子所受的力來驅使粒子移動及使粒子的屬性變化。對應就上面的第二點,也是整個粒子系統的核心部分。

ParticleMemory:粒子內存池,由於粒子系統伴隨着大量粒子的創建和移除。所以一開始就創建一定數量的粒子,需要(創建)粒子的時候返回粒子索引即可。

ParticleDescription:粒子描述,擁有創建粒子系統的數據。也就是用來初始化 ParticleSystem 和 ParticleEmitter 對象的結構數據。

Particle:粒子對象,擁有粒子屬性數據,可以通過渲染器渲染粒子顯示出來。其結構如下

    struct Particle
    {
        Vec2 vPos;
        Vec2 vChangePos;
        Vec2 vStartPos;

        Color cColor;
        Color cDeltaColor;

        float fCurrentSize;
        float fSize;
        float fDeltaSize;

        float fRotation;
        float fDeltaRotation;

        float fRemainingLife;

        /* 重力模式數據 */
        struct GravityModeData
        {
            Vec2  vInitialValocity;        /* 初速度 */
            float fRadialAccel;            /* 徑向加速度(法相加速度), 與運動方向垂直 */
            float fTangentialAccel;        /* 切向加速度 */

        } gravityMode;

        /* 半徑模式數據 */
        struct RadiusModeData
        {
            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 = new Particle;
            vParticlePool.push_back(particle);
            vUnusedParticleList.push_back(particle);
        }

需要生成粒子時,從存儲着未被使用的粒子的粒子容器 vUnusedParticleList 中取出粒子

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

銷毀粒子時

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

粒子池的實現較為簡單

 

粒子發射器 ParticleEmitter

發射器只主要做的工作就是生成粒子(不負責銷毀生命已經結束的粒子),給發射器添加一個屬性——發射速率 emitRate(每秒發射粒子的數量),發射器就按照發射速率來發射粒子。

    void ParticleEmitter::emitParticles(float dt)
    {
        /* 發射一個粒子所用時間 */
        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 初始化粒子

    void ParticleEmitter::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, float dt)
    {
        std::list<Particle*>* indexList = pe->getParticleList();

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

            p->fRemainingLife -= dt;

            if ( p->fRemainingLife > 0 ) {
                static Vec2 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 class MotionMode
    {
        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, float dt)
    {
        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"

namespace Simple2D
{
    /* 發射器類型 */
    enum class EmitterType
    {
        EMITTER_TYPE_GRAVITY,     /* 重力模式 */
        EMITTER_TYPE_RADIUS        /* 半徑模式 */
    };

    /* 粒子運動模式 */
    enum class MotionMode
    {
        MOTION_MODE_FREE,        /* 粒子運動和發射器無關 */
        MOTION_MODE_RELATIVE     /* 粒子運動跟隨發射器位置 */
    };

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

        float fTangentialAccel;        /* 切向加速度 */
        float fTangentialAccelVar;     /* 徑向加速度變化值 */

        float fRadialAccel;            /* 徑向加速度 */
        float fRadialAccelVar;         /* 徑向加速度變化值 */
    };

    /* 半徑模式 */
    struct RadiusMode
    {
        float fBeginRadius;           /* 起始半徑  */
        float fBeginRadiusVar;        /* 起始半徑變化值 */

        float fEndRadius;           /* 結束半徑 */
        float fEndRadiusVar;        /* 結束半徑變化值 */

        float fSpinPerSecond;       /* 每秒旋轉角度 */
        float fSpinPerSecondVar;    /* 每秒旋轉角度變化值 */
    };


    class DLL_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;            /* 發射器發射粒子角度 */
        float fEmitAngleVar;
                
        float fEmitSpeed;            /* 發射器發射粒子速度 */
        float fEmitSpeedVar;

        int nParticleCount;         /* 粒子數量 */
        float fEmitRate;            /* 粒子每秒發射速率 */
        float fDuration;            /* 發射器發射粒子時間 */

        EmitterType emitterType;
        MotionMode  motionMode;

        /* 粒子屬性 */

        /* 粒子生命周期 */
        float fLife;
        float fLifeVar;

        /* 粒子的顏色變化 */
        Color cBeginColor;
        Color cBeginColorVar;
        Color cEndColor;
        Color cEndColorVar;

        /* 粒子的大小變化 */
        float fBeginSize;
        float fBeginSizeVar;
        float fEndSize;
        float fEndSizeVar;

        /* 粒子旋轉角度變化 */
        float fBeginSpin;
        float fBeginSpinVar;
        float fEndSpin;
        float fEndSpinVar;

        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 = new GravityParticleEffect();
        }
        else {
            effect = new RadialParticleEffect();
        }

        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::string tmpstr1, 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));
            }
        }
        return particleConfigMap;
    }

將解析后的數據保存到一個映射表中

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());

        return desc;
    }
#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;
        }

        static RenderUnit 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(float dt)
    {
        for ( auto& ele : vParticleSystems ) {
            ele->update(dt);
        }
    }

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

 

最后在主函數中使用粒子系統

    ParticleSystemManager particleSystemManager;

    ParticleSystem* fire1PS = new ParticleSystem;
    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 = new ParticleSystem;
    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 = new ParticleSystem;
    radius1PS->initWithPlist("Particle/radius1.plist");
    radius1PS->setTexture("Particle/fire.png");
    radius1PS->getEmitter()->setEmitPos(Vec2(400, 420));

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

    ParticleSystem* starPS = new ParticleSystem;
    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 = new ParticleSystem;
    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 = new ParticleSystem;
    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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM