基于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 = ; i < size; i++ ) {
particle = new Particle;
vParticlePool.push_back(particle);
vUnusedParticleList.push_back(particle);
}
需要生成粒子时,从存储着未被使用的粒子的粒子容器 vUnusedParticleList 中取出粒子
Particle* ParticleMemory::allocParticle()
{
if ( (nFreeIndex >= vParticlePool.size() - ) ) {
return nullptr;
}
else {
return vUnusedParticleList[nFreeIndex++];
}
}
销毁粒子时
void ParticleMemory::freeParticle(Particle* particle)
{
assert(nFreeIndex != );
vUnusedParticleList[--nFreeIndex] = particle;
}
粒子池的实现较为简单
粒子发射器 ParticleEmitter
发射器只主要做的工作就是生成粒子(不负责销毁生命已经结束的粒子),给发射器添加一个属性——发射速率 emitRate(每秒发射粒子的数量),发射器就按照发射速率来发射粒子。
void ParticleEmitter::emitParticles(float dt)
{
/* 发射一个粒子所用时间 */
float emit_particle_time = / emitRate; /* 累计发射时间 */
if ( vParticleList.size() < particleCount ) {
fEmitCounter += dt;
} /* 在时间 emit_counter 发射 emit_counter / rate 个粒子 */
while ( vParticleList.size() < particleCount && fEmitCounter > ) {
this->addParticle();
fEmitCounter -= emit_particle_time;
} fElapsed += dt;
if ( duration != - && duration < fElapsed ) {
fElapsed = ;
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(), , );
begin_color.g = CLAMPF(beginColor.g + beginColorVar.g * RANDOM_MINUS1_1(), , );
begin_color.b = CLAMPF(beginColor.b + beginColorVar.b * RANDOM_MINUS1_1(), , );
begin_color.a = CLAMPF(beginColor.a + beginColorVar.a * RANDOM_MINUS1_1(), , ); end_color.r = CLAMPF(endColor.r + endColorVar.r * RANDOM_MINUS1_1(), , );
end_color.g = CLAMPF(endColor.g + endColorVar.g * RANDOM_MINUS1_1(), , );
end_color.b = CLAMPF(endColor.b + endColorVar.b * RANDOM_MINUS1_1(), , );
end_color.a = CLAMPF(endColor.a + endColorVar.a * RANDOM_MINUS1_1(), , ); float tmp = / (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(, beginSize + beginSizeVar * RANDOM_MINUS1_1());
float end_size = MAX(, endSize + endSizeVar * RANDOM_MINUS1_1()); particle->fSize = begin_size;
particle->fDeltaSize = (end_size - begin_size) / particle->fRemainingLife; /* 粒子旋转角度 */
float begin_spin = toRadian(MAX(, beginSpin + beginSpinVar * RANDOM_MINUS1_1()));
float end_spin = toRadian(MAX(, 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 > ) {
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(, 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 > ) {
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(, 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(, )
, vEmitPosVar(, )
, fEmitAngle()
, fEmitAngleVar()
, fEmitSpeed()
, fEmitSpeedVar()
, nParticleCount()
, fEmitRate()
, fDuration(-)
, emitterType(EmitterType::EMITTER_TYPE_GRAVITY)
, motionMode(MotionMode::MOTION_MODE_FREE)
, fLife()
, fLifeVar()
, cBeginColor(, , , )
, cBeginColorVar(, , , )
, cEndColor(, , , )
, cEndColorVar(, , , )
, fBeginSize()
, fBeginSizeVar()
, fEndSize()
, fEndSizeVar()
, fBeginSpin()
, fBeginSpinVar()
, fEndSpin()
, fEndSpinVar()
{
gravityMode.fRadialAccel = ;
gravityMode.fRadialAccelVar = ;
gravityMode.fTangentialAccel = ;
gravityMode.fTangentialAccelVar = ;
gravityMode.vGravity.set(, ); radiusMode.fBeginRadius = ;
radiusMode.fBeginRadiusVar = ;
radiusMode.fEndRadius = ;
radiusMode.fEndRadiusVar = ;
radiusMode.fSpinPerSecond = ;
radiusMode.fSpinPerSecondVar = ;
} /* 发射器属性 */ 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()) == ) {
ele = ele->NextSiblingElement()->NextSiblingElement();
}
else {
tmpstr1 = ele->GetText();
ele = ele->NextSiblingElement();
tmpstr2 = ele->GetText() == nullptr ? "" : 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[] = {
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 = ;
float s = , c = , x = , y = ; auto particleIndex = pEmitter->getParticleList();
Particle* particle = nullptr; int count = particleIndex->size();
if ( vPositions.size() < count * ) {
vPositions.resize(count * );
vColors.resize(count * );
} nPositionIndex = ;
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 + ].set(x - c - s, y - c + s, );
vPositions[nPositionIndex + ].set(x - c + s, y + c + s, );
vPositions[nPositionIndex + ].set(x + c + s, y + c - s, );
vPositions[nPositionIndex + ].set(x + c - s, y - c - s, ); vColors[nPositionIndex + ] = particle->cColor;
vColors[nPositionIndex + ] = particle->cColor;
vColors[nPositionIndex + ] = particle->cColor;
vColors[nPositionIndex + ] = particle->cColor; nPositionIndex += ;
} static RenderUnit unit;
unit.pPositions = &vPositions[];
unit.nPositionCount = nPositionIndex;
unit.nIndexCount = nPositionIndex * 1.5;
unit.color = &vColors[];
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(, ));
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(, ));
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(, )); ParticleSystem* radius2PS = new ParticleSystem;
radius2PS->initWithPlist("Particle/radius2.plist");
radius2PS->setTexture("Particle/fire.png");
radius2PS->getEmitter()->setEmitPos(Vec2(, )); ParticleSystem* starPS = new ParticleSystem;
starPS->initWithPlist("Particle/star.plist");
starPS->setTexture("Particle/star.png");
starPS->getEmitter()->setEmitPos(Vec2(, ));
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(, ));
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(, )); 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 / );
particleSystemManager.render(graphicsContext.getRenderer());
源码下载:http://pan.baidu.com/s/1skOmP21
基于OpenGL编写一个简易的2D渲染框架-06 编写一个粒子系统的更多相关文章
- 基于OpenGL编写一个简易的2D渲染框架-05 渲染文本
阅读文章前需要了解的知识:文本渲染 https://learnopengl-cn.github.io/06%20In%20Practice/02%20Text%20Rendering/ 简要步骤: 获 ...
- 基于OpenGL编写一个简易的2D渲染框架-01 创建窗口
最近正在学习OpenGL,我认为学习的最快方法就是做一个小项目了. 如果对OpenGL感兴趣的话,这里推荐一个很好的学习网站 https://learnopengl-cn.github.io/ 我用的 ...
- 基于OpenGL编写一个简易的2D渲染框架-03 渲染基本几何图形
阅读文章前需要了解的知识,你好,三角形:https://learnopengl-cn.github.io/01%20Getting%20started/04%20Hello%20Triangle/ 要 ...
- 基于OpenGL编写一个简易的2D渲染框架-08 重构渲染器-整体架构
事实上,前面编写的渲染器 Renderer 非常简陋,虽然能够进行一些简单的渲染,但是它并不能满足我们的要求. 当渲染粒子系统时,需要开启混合模式,但渲染其他顶点时却不需要开启混合模式.所以同时渲染粒 ...
- 基于OpenGL编写一个简易的2D渲染框架-04 绘制图片
阅读文章前需要了解的知识,纹理:https://learnopengl-cn.github.io/01%20Getting%20started/06%20Textures/ 过程简述:利用 FreeI ...
- 基于OpenGL编写一个简易的2D渲染框架-09 重构渲染器-Shader
Shader 只是进行一些简单的封装,主要功能: 1.编译着色程序 2.绑定 Uniform 数据 3.根据着色程序的顶点属性传递顶点数据到 GPU 着色程序的编译 GLuint Shader::cr ...
- 基于OpenGL编写一个简易的2D渲染框架-02 搭建OpenGL环境
由于没有使用GLFW库,接下来得费一番功夫. 阅读这篇文章前请看一下这个网页:https://learnopengl-cn.github.io/01%20Getting%20started/02%20 ...
- 基于OpenGL编写一个简易的2D渲染框架-11 重构渲染器-Renderer
假如要渲染一个纯色矩形在窗口上,应该怎么做? 先确定顶点的格式,一个顶点应该包含位置信息 vec3 以及颜色信息 vec4,所以顶点的结构体定义可以这样: struct Vertex { Vec3 p ...
- 基于OpenGL编写一个简易的2D渲染框架-10 重构渲染器-Pass
Pass,渲染通路,一个渲染通路指的是一次像素处理和一次顶点处理,也就是指的是一次绘制.简单来说就是顶点数据在渲染管线中走一遍最后绘制. 渲染粒子系统的粒子时,需要开启 OpenGL 的混合模式,并使 ...
随机推荐
- vulcanjs 核心架构概念
基于包的架构 为了保证系统的灵活以及可扩展,vulcanjs 使用基于包的架构设计,每一个功能都是一个包,可以方便的添加,移除 扩展.而不是修改 vulcan 的设计哲学是进行系统扩展,而不是编辑修改 ...
- nodejs express 学习
nodejs的大名好多人应该是听过的,而作为nodejs web 开发的框架express 大家也应该比较熟悉. 记录一下关于express API 的文档: express() 创建express ...
- 修改oracle系统参数spfile导致数据库无法启动解决
错误示范: SQL> alter system set nls_date_format='yyyy-mm-dd 24hh:mi:ss' scope=spfile;System altered.我 ...
- selenium启动谷歌所遇到的问题
最近在学习selenium webdriver,发现启动火狐时,运行非常慢,几天前一直在尝试启动谷歌驱动启动,但启动中总遇到问题,启动不起来,一直百度查找方法,还是没搞定,个人比较执着,爱钻牛角尖,弄 ...
- 【appium】根据UIAutomator定位元素
text属性的方法 driver.find_element_by_android_uiautomator('new UiSelector().text("Custom View") ...
- Action<T>和Func<T>委托
Action<T>和Func<T>委托 泛型Action<T>委托和Func<T>委托是系统定义的两个泛型委托. Action<T>委托表示 ...
- C++将整型数据转换成大端或小端存储顺序
大端和小端的概念参考之前博客: 大端/小端,高字节/低字节,高地址/低地址,移位运算 昨晚帮导师从指令中恢复图像的时候,导师要我转换成raw格式,也就是记录图像像素的二进制序列,然后反复强调让我注意大 ...
- Python网络爬虫-xpath模块
一.正解解析 单字符: . : 除换行以外所有字符 [] :[aoe] [a-w] 匹配集合中任意一个字符 \d :数字 [0-9] \D : 非数字 \w :数字.字母.下划线.中文 \W : 非\ ...
- Java堆外内存之二:堆外内存使用总结
目录: <堆外内存操作类ByteBuffer> <DirectBuffer> <Unsafe(java可直接操作内存(),挂起与恢复,CAS操作)> 有时候对内存进 ...
- 正则的使用及replace细讲
1.var reg=/./ 与 var reg=/\./的区别? .代表任意一个字符 \.而后者代表这个字符串中得有一个. 2.?的使用 如果单独的一个字符后面带? /\d?/ 代表1个或0个这个字符 ...