cocos2dx粒子引擎的分析-2

粒子系统分为发射器和粒子,发射器的作用就是获取配置文件中的各种数据,然后在特定的时刻产生粒子,并为该粒子赋值,而为了让粒子更加具有随机性,而采用了*Variance变量和CC_RANDOM_MINUS1_!()来保证随机性。

接下来则主要分析粒子的产生、更新和结束。

先看addParticles(int count),这个函数的作用有两个,第一个开辟出足够的空间(只是单纯的增加了_aprticleCount这个变量);第二个作用则是为添加的粒子进行赋值。

void ParticleSystem::addParticles(int count)
{
    if (_paused)
        return;
    //获取随机种子
    uint32_t RANDSEED = rand();
    //保存当前的粒子数目,从该位置开始对粒子赋值
    int start = _particleCount;
    _particleCount += count;

    //life
    for (int i = start; i < _particleCount ; ++i)
    {
        float theLife = _life + _lifeVar * RANDOM_M11(&RANDSEED);
        _particleData.timeToLive[i] = MAX(0, theLife);
    }

    //position
    for (int i = start; i < _particleCount; ++i)
    {
        _particleData.posx[i] = _sourcePosition.x + _posVar.x * RANDOM_M11(&RANDSEED);
    }

    for (int i = start; i < _particleCount; ++i)
    {
        _particleData.posy[i] = _sourcePosition.y + _posVar.y * RANDOM_M11(&RANDSEED);
    }

这里面的CC_RANDOM_M11()定义如下

/**
 A more effect random number getter function, get from ejoy2d.
 */
inline static float RANDOM_M11(unsigned int *seed) {
    *seed = *seed * 134775813 + 1;
    union {
        uint32_t d;         
        float f;
    } u;
    u.d = (((uint32_t)(*seed) & 0x7fff) << 8) | 0x40000000;
    return u.f - 3.0f;
}

官方说这是一个更加有效的随机数字生成;而2.x时代使用的是CC_RANDOM_MINUS1_1()这个函数,应该功能相同,都是获取[-1,1]的一个随机值。

这里还需要注意下_sourcePosition的值,按照上一节的分析来说,这个值并不会被改变,所以在这里_sourcePosition应该为(0,0)才对。

#define SET_COLOR(c, b, v)\
for (int i = start; i < _particleCount; ++i)\
{\
c[i] = clampf( b + v * RANDOM_M11(&RANDSEED) , 0 , 1 );\
}
    
    SET_COLOR(_particleData.colorR, _startColor.r, _startColorVar.r);
    SET_COLOR(_particleData.colorG, _startColor.g, _startColorVar.g);
    SET_COLOR(_particleData.colorB, _startColor.b, _startColorVar.b);
    SET_COLOR(_particleData.colorA, _startColor.a, _startColorVar.a);
    
    SET_COLOR(_particleData.deltaColorR, _endColor.r, _endColorVar.r);
    SET_COLOR(_particleData.deltaColorG, _endColor.g, _endColorVar.g);
    SET_COLOR(_particleData.deltaColorB, _endColor.b, _endColorVar.b);
    SET_COLOR(_particleData.deltaColorA, _endColor.a, _endColorVar.a);

clamp(x, min, max)的作用是一个判断语句:

template<typename T>
T clamp(T x, T min, T max)
{
    if (x < min)
        return min;
    else if (x > max)
        return max;
    return x;
}
#define SET_DELTA_COLOR(c, dc)\
for (int i = start; i < _particleCount; ++i)\
{\
dc[i] = (dc[i] - c[i]) / _particleData.timeToLive[i];\
}
    
    SET_DELTA_COLOR(_particleData.colorR, _particleData.deltaColorR);
    SET_DELTA_COLOR(_particleData.colorG, _particleData.deltaColorG);
    SET_DELTA_COLOR(_particleData.colorB, _particleData.deltaColorB);
    SET_DELTA_COLOR(_particleData.colorA, _particleData.deltaColorA);

    //size
    for (int i = start; i < _particleCount; ++i)
    {
        _particleData.size[i] = _startSize + _startSizeVar * RANDOM_M11(&RANDSEED);
        _particleData.size[i] = MAX(0, _particleData.size[i]);
    }
    //特殊化处理,如果_startSize == _endSize,则必有_deltaSize = 0.f
    if (_endSize != START_SIZE_EQUAL_TO_END_SIZE)
    {
        for (int i = start; i < _particleCount; ++i)
        {
            float endSize = _endSize + _endSizeVar * RANDOM_M11(&RANDSEED);
            endSize = MAX(0, endSize);
            _particleData.deltaSize[i] = (endSize - _particleData.size[i]) / _particleData.timeToLive[i];
        }
    }
    else
    {
        for (int i = start; i < _particleCount; ++i)
        {
            _particleData.deltaSize[i] = 0.0f;
        }
    }
   // rotation旋转
    for (int i = start; i < _particleCount; ++i)
    {
        _particleData.rotation[i] = _startSpin + _startSpinVar * RANDOM_M11(&RANDSEED);
    }
    for (int i = start; i < _particleCount; ++i)
    {
        float endA = _endSpin + _endSpinVar * RANDOM_M11(&RANDSEED);
        _particleData.deltaRotation[i] = (endA - _particleData.rotation[i]) / _particleData.timeToLive[i];
    }

上面设置了color、size、rotation等变量,注意以下上面的Delta类变量的值和该粒子的timeToLive有关,timeToLive表示生命值,那么上面无论是color、size还是rotation的变化都是平hua的变化的。

    // position
    Vec2 pos;
    if (_positionType == PositionType::FREE)
    {
        pos = this->convertToWorldSpace(Vec2::ZERO);
    }
    else if (_positionType == PositionType::RELATIVE)
    {
        pos = _position;
    }
    for (int i = start; i < _particleCount; ++i)
    {
        _particleData.startPosX[i] = pos.x;
    }
    for (int i = start; i < _particleCount; ++i)
    {
        _particleData.startPosY[i] = pos.y;
    }

在前面设置了sourcePosition,这里设置了开始位置,暂时没有发现他们之间的区别。

   // Mode Gravity: A
    if (_emitterMode == Mode::GRAVITY)
    {

        // radial accel
        for (int i = start; i < _particleCount; ++i)
        {
            _particleData.modeA.radialAccel[i] = modeA.radialAccel + modeA.radialAccelVar * RANDOM_M11(&RANDSEED);
        }

        // tangential accel
        for (int i = start; i < _particleCount; ++i)
        {
            _particleData.modeA.tangentialAccel[i] = modeA.tangentialAccel + modeA.tangentialAccelVar * RANDOM_M11(&RANDSEED);
        }

        // rotation is dir
        if( modeA.rotationIsDir )
        {
            for (int i = start; i < _particleCount; ++i)
            {
               float a = CC_DEGREES_TO_RADIANS( _angle + _angleVar * RANDOM_M11(&RANDSEED) );
                Vec2 v(cosf( a ), sinf( a ));
                float s = modeA.speed + modeA.speedVar * RANDOM_M11(&RANDSEED);
                Vec2 dir = v * s;
                _particleData.modeA.dirX[i] = dir.x;//v * s ;
                _particleData.modeA.dirY[i] = dir.y;
                _particleData.rotation[i] = -CC_RADIANS_TO_DEGREES(dir.getAngle());
            }
        }
        else
        {
            for (int i = start; i < _particleCount; ++i)
            {
                float a = CC_DEGREES_TO_RADIANS( _angle + _angleVar * RANDOM_M11(&RANDSEED) );
                Vec2 v(cosf( a ), sinf( a ));
                float s = modeA.speed + modeA.speedVar * RANDOM_M11(&RANDSEED);
                Vec2 dir = v * s;
                _particleData.modeA.dirX[i] = dir.x;//v * s ;
                _particleData.modeA.dirY[i] = dir.y;
            }
        }

    }

设置了重力模式下的一些变量。注意这里的CC_DEGREES_TO_RADIANS(),这个hong函数是把角度转化成弧度,而所谓的方向dir就是sinf() cosf()得到的速度分量。

    // Mode Radius: B
    else
    {
        //Need to check by Jacky
        // Set the default diameter of the particle from the source position
        for (int i = start; i < _particleCount; ++i)
        {
            _particleData.modeB.radius[i] = modeB.startRadius + modeB.startRadiusVar * RANDOM_M11(&RANDSEED);
        }
        //particleData.modeB.angle保存的是弧度,而配置文件中保存的是角度
        //不过angle的翻译不是角度? 
        for (int i = start; i < _particleCount; ++i)
        {
            _particleData.modeB.angle[i] = CC_DEGREES_TO_RADIANS( _angle + _angleVar * RANDOM_M11(&RANDSEED));
        }

        for (int i = start; i < _particleCount; ++i)
        {
            _particleData.modeB.degreesPerSecond[i] = CC_DEGREES_TO_RADIANS(modeB.rotatePerSecond + modeB.rotatePerSecondVar * RANDOM_M11(&RANDSEED));
        }
       if(modeB.endRadius == START_RADIUS_EQUAL_TO_END_RADIUS)
        {
            for (int i = start; i < _particleCount; ++i)
            {
                _particleData.modeB.deltaRadius[i] = 0.0f;
            }
        }
        else
        {
            for (int i = start; i < _particleCount; ++i)
            {
                float endRadius = modeB.endRadius + modeB.endRadiusVar * RANDOM_M11(&RANDSEED);
                _particleData.modeB.deltaRadius[i] = (endRadius - _particleData.modeB.radius[i]) / _particleData.timeToLive[i];
            }
        }
    }
}

addParticles(int)这个函数的功能说简单dian就是为一定数量的粒子进行变量的赋值。

void ParticleSystem::stopSystem()
{
    _isActive = false;
    _elapsed = _duration;
    _emitCounter = 0;
}

stopSystem()函数中所谓的停止只是对变量的赋值,那是如何让粒子发射器不再发射粒子和进行更新了呢?主要还是在于update()这个函数。

void ParticleSystem::resetSystem()
{
    _isActive = true;
    _elapsed = 0;
    for (int i = 0; i < _particleCount; ++i)
    {
        _particleData.timeToLive[i] = 0.0f;
    }
}

resetSystem()只是让_elapsed=0,以及前_particleCount个粒子的生命值全部为0,那么这些粒子会在update中进行收回。

接下来看看update函数

void ParticleSystem::update(float dt)
{
    CC_PROFILER_START_CATEGORY(kProfilerCategoryParticles , "CCParticleSystem - update");
    //当前处于活动状态且_emissionRate != 0.f
    if (_isActive && _emissionRate)
    {
        //等同于float rate = _life / _totalParticles;
        //计算出每个粒子的产出平均时间间隔
        float rate = 1.0f / _emissionRate;
        int totalParticles = static_cast<int>(_totalParticles * __totalParticleCountFactor);

        //issue #1201, prevent bursts of particles, due to too high emitCounter
        //避免粒子的喷发
        if (_particleCount < totalParticles)
        {
            _emitCounter += dt;
            if (_emitCounter < 0.f)
                _emitCounter = 0.f;
        }
        //获取当前要发射的粒子的数目
        int emitCount = MIN(totalParticles - _particleCount, _emitCounter / rate);
        //添加粒子
        addParticles(emitCount);
        _emitCounter -= rate * emitCount;

        _elapsed += dt;
       if (_elapsed < 0.f)
            _elapsed = 0.f;
        //如果_duration != -1 且持续时间已经被_elapsed超过,则停止系统
        //这个_duration表示的和Action的持续时间类似
        if (_duration != DURATION_INFINITY && _duration < _elapsed)
        {
            this->stopSystem();
        }
    }

int emitCount = MIN(totalParticles - _particleCount, _emitCounter / rate);

Min的作用是避免产生的粒子数量超过totalParticles - _particleCount,rate表示产生粒子的时间间隔,那么_emitCounter / rate则是在这个间隔下要产出的粒子的数目是多少。

_emitCounter -= rate * emitCount;

则是减少发射器计数器的时间,这也就是为什么会有以下代码的原因

        if (_particleCount < totalParticles)
        {
            _emitCounter += dt;
            if (_emitCounter < 0.f)
                _emitCounter = 0.f;
        }

接着往下进行分析

   {//不太懂这个{}的意思
        //减去每个粒子的生命值
        for (int i = 0; i < _particleCount; ++i)
        {
            _particleData.timeToLive[i] -= dt;
        }
        //如果存在粒子的生命值为0
        for (int i = 0; i < _particleCount; ++i)
        {
            if (_particleData.timeToLive[i] <= 0.0f)
            {
                //从后向前找到一个生命值大于0的粒子
                int j = _particleCount - 1;
                while (j > 0 && _particleData.timeToLive[j] <= 0)
                {
                    _particleCount--;
                    j--;
                }
                //把这个粒子赋值给前面的粒子
                _particleData.copyParticle(i, _particleCount - 1);
                if (_batchNode)
                {
                    //disable the switched particle
                    int currentIndex = _particleData.atlasIndex[i];
                    _batchNode->disableParticle(_atlasIndex + currentIndex);
                    //switch indexes
                    _particleData.atlasIndex[_particleCount - 1] = currentIndex;
                }
                //相当于删除第 particleCount - 1那个粒子,因为其不再进行更新
                --_particleCount;
                //粒子死光且在之后自动删除自己,则先停止update函数的调用
                //然后删除自己
                if( _particleCount == 0 && _isAutoRemoveOnFinish )
                {
                    this->unscheduleUpdate();
                    _parent->removeChild(this, true);
                    return;
                }
            }
        }

这一块进行的主要是对“死亡”粒子的更新和粒子数组的安排,用的方法很是巧妙。

        //如果是重力模式,则进行重力模式相关变量的更新
        //根据上面的代码,能保证当前的粒子数组生命值都大于0
        if (_emitterMode == Mode::GRAVITY)
        {
            for (int i = 0 ; i < _particleCount; ++i) 
            {
                particle_point tmp, radial = {0.0f, 0.0f}, tangential;
     
                // radial acceleration
                if (_particleData.posx[i] || _particleData.posy[i])
                {
                    normalize_point(_particleData.posx[i], _particleData.posy[i], &radial);
                }
                tangential = radial;
                radial.x *= _particleData.modeA.radialAccel[i];
                radial.y *= _particleData.modeA.radialAccel[i];
     
                // tangential acceleration
                std::swap(tangential.x, tangential.y);
                tangential.x *= - _particleData.modeA.tangentialAccel[i];
                tangential.y *= _particleData.modeA.tangentialAccel[i];
     
                // (gravity + radial + tangential) * dt
                tmp.x = radial.x + tangential.x + modeA.gravity.x;
                tmp.y = radial.y + tangential.y + modeA.gravity.y;
                tmp.x *= dt;
                tmp.y *= dt;

                _particleData.modeA.dirX[i] += tmp.x;
                _particleData.modeA.dirY[i] += tmp.y;

                // this is cocos2d-x v3.0
                // if (_configName.length()>0 && _yCoordFlipped != -1)

                // this is cocos2d-x v3.0
                tmp.x = _particleData.modeA.dirX[i] * dt * _yCoordFlipped;
                tmp.y = _particleData.modeA.dirY[i] * dt * _yCoordFlipped;
                _particleData.posx[i] += tmp.x;
                _particleData.posy[i] += tmp.y;
            }
        }
       else
        {
            //Why use so many for-loop separately instead of putting them together?
            //When the processor needs to read from or write to a location in memory,
            //it first checks whether a copy of that data is in the cache.
            //And every property's memory of the particle system is continuous,
            //for the purpose of improving cache hit rate, we should process only one property in one for-loop AFAP.
            //It was proved to be effective especially for low-end machine. 
            //对低端机 的优化??
            for (int i = 0; i < _particleCount; ++i)
            {
                _particleData.modeB.angle[i] += _particleData.modeB.degreesPerSecond[i] * dt;
            }

            for (int i = 0; i < _particleCount; ++i)
            {
                _particleData.modeB.radius[i] += _particleData.modeB.deltaRadius[i] * dt;
            }

            for (int i = 0; i < _particleCount; ++i)
            {
                _particleData.posx[i] = - cosf(_particleData.modeB.angle[i]) * _particleData.modeB.radius[i];
            }
           for (int i = 0; i < _particleCount; ++i)
            {
                _particleData.posy[i] = - sinf(_particleData.modeB.angle[i]) * _particleData.modeB.radius[i] * _yCoordFlipped;
            }
        }

这个是环形模式下的变量的更新,不过看注释说为了低端机,所以每个循环仅仅处理一个字段。没有测试过,就不多说了。

        //color r,g,b,a
        for (int i = 0 ; i < _particleCount; ++i)
        {
            _particleData.colorR[i] += _particleData.deltaColorR[i] * dt;
        }

        for (int i = 0 ; i < _particleCount; ++i)
        {
            _particleData.colorG[i] += _particleData.deltaColorG[i] * dt;
        }

        for (int i = 0 ; i < _particleCount; ++i)
        {
            _particleData.colorB[i] += _particleData.deltaColorB[i] * dt;
        }

        for (int i = 0 ; i < _particleCount; ++i)
        {
            _particleData.colorA[i] += _particleData.deltaColorA[i] * dt;
        }
        //size
        for (int i = 0 ; i < _particleCount; ++i)
        {
            _particleData.size[i] += (_particleData.deltaSize[i] * dt);
            _particleData.size[i] = MAX(0, _particleData.size[i]);
        }
        //angle
        for (int i = 0 ; i < _particleCount; ++i)
        {
            _particleData.rotation[i] += _particleData.deltaRotation[i] * dt;
        }

        updateParticleQuads();
        _transformSystemDirty = false;
    }
}//end bolck

这一块进行的是通用属性的更新。

   // only update gl buffer when visible
    if (_visible && ! _batchNode)
    {
        postStep();
    }

    CC_PROFILER_STOP_CATEGORY(kProfilerCategoryParticles , "CCParticleSystem - update");
}

这一块则是为其子类留下的一个接口。

ParticleSystem的分析基本结束,上面主要讲到了粒子是通过addParticles()这个函数产生的、在update中的操作主要是先根据流逝的时间产出对应的粒子,然后减少每个粒子的生命值,之后收回已经死亡的粒子,然后才对剩下的粒子进行属性更新。

这点和2.x不一样,在2.x中,减少生命值、删除生命值小于0的元素以及进行粒子的更新是在一块的,部分代码如下:

       //如果显示标记为true
        if (this->isVisible())
        {
                //遍历每一个粒子,进行更新
                while (_particleIdx < _particleCount)
                {
                        //取得对应的粒子
                        Particle* p = &_pParticles[_particleIdx];
                        //减少生命值
                        p->timeToLive -= dt; 
                        //生命值大于0
                        if (p->timeToLive > 0.f)
                        {
                                //重力加速度模式
                                if (_emitterMode == Mode::GRAVITY)
                                {
                                        Point tmp, radial, tangential;
                                        //旋转角度的计算 向量归一化
                                        if (p->pos.x || p->pos.y)
                                        {
                                                radial = p->pos;
                                                radial.normalize();
                                        }
                                        tangential = radial;
                                        //获取旋转角度
                                        radial = radial * p->modeA.radialAccel;
                                        //切角度变化计算
                                        float newy = tangential.x;
                                        tangential.x = -tangential.x;
                                        tangential.y = newy;

                                        tangential = tangential * p->modeA.tangentialAccel;
                                        //位置的计算
                                        tmp = radial + tangential + modeA.gravity;
                                        tmp = tmp * dt; 
                                        p->modeA.dir = p->modeA.dir + tmp;
                                        tmp = p->modeA.dir * dt; 

                                        p->pos = p->pos + tmp;
                                }
                                else//环形模式
                                {
                                        //更新粒子的角度
                                        p->modeB.angle += p->modeB.degreePerSec * dt; 
                                        p->modeB.radius += p->modeB.deltaRadius * dt; 
                                        //计算粒子的位置
                                        p->pos.x = -cosf(p->modeB.angle) * p->modeB.radius;
                                        p->pos.y = -sinf(p->modeB.angle) * p->modeB.radius;
                                }
                                //颜色的变化计算
                                p->color.r += p->deltaColor.r * dt;
                                p->color.g += p->deltaColor.g * dt;
                                p->color.b += p->deltaColor.b * dt;

                                //大小的变化计算
                                p->size += p->deltaSize * dt;
                                p->size = MAX(0, p->size);
                                //角度
                                p->rotation += p->deltaRotation * dt;

                                Point newPos;
                                //是自由模式或相对模式
                                if (_positionType == PositionType::FREE
                                || _positionType == PositionType::RELATIVE)
                                {
                                        Point diff = currentPosition - p->startPos;
                                        newPos = p->pos - diff;
                                }
                                else
                                {
                                        newPos = p->pos;
                                }
                                //TODO:应该要加
                                newPos += _position;
                                ++_particleIdx;
                        }//生命值小于0
                        else
                        {
                                if (_particleIdx != _particleCount - 1)
                                {
                                        _pParticles[_particleIdx] = _pParticles[_particleCount - 1];
                                }
                                --_particleCount;
                                //如果粒子数量为0,则设置自动删除粒子系统
                                if (_particleCount == 0 && _isAutoRemoveOnFinish)
                                {
                                        //this->unscheduleUpdate();
                                        this->removeFromParent();
                                        return ;
                                }
                        }
                }
                _transformSystemDirty = false;
        }
}

尤其在于对粒子的删除操作上,2.x默认认为后面那部分的粒子是活zhe的,然后直接进行了交换,而此时的_articleIdx并没有增加,那么下一次循环依然是这个粒子,而3.x则是分成了数个for循环。

我虽然借助cocos2dx实现了一个粒子引擎,但是我发现一个粒子文件一般要渲染数百次,对有批次渲染的还好,没有批次渲染的就差得多了。。。

猜你喜欢

转载自blog.csdn.net/bull521/article/details/82720679
今日推荐