用DirectX实现粒子系统(三)
引言
上一篇讲了如何在DirectX中使用点精灵,这篇再扩展一下,讲讲如何实现一个完整的粒子系统效果-烟花。任何一种复杂的现象都可以拆分为若干独立的小单元,烟花也是如此,一个绚丽的烟花无非是若干小粒子按照一定的顺序,一定的速度,颜色及存活时间组合而成的。烟花的种类成千上万,不同的烟花有不同的效果,我们今天主要讲述爆炸效果。先来几张效果图吧,如下。
前面说了,一个复杂的现象是由若干个基本单元构成的,所以,我们最先定义这个基本单元-粒子,一个粒子主要有哪些属性呢?由上面的图可知,首先粒子是有颜色的,上面一共有白,红,黄,绿四种颜色的粒子。其次,粒子是有大小的,只有大小不同的粒子相互组合才能构成特殊的渐进效果。再次粒子也是有纹理的,比如上面的效果中,我们一共使用了如下三种纹理
粒子也是有位置的,给粒子设定一个初始位置,然后按照时间不断变化粒子的位置才能形成特殊的效果。粒子也是有生命周期的,因为随着时间的流逝,粒子会渐渐变暗,最终消亡,消亡的粒子将不再被渲染。运动的粒子还要有一个初始速度及加速度。就这些了!下面列出了一个粒子应该具备的所有属性。
- 颜色
- 大小
- 纹理
- 位置
- 生命周期
- 初始速度
- 加速度
所以,我们可以如下定义一个粒子类。
class Particle { public: Particle(void); virtual ~Particle(void); public: bool m_isLive ; // Draw particle when isLive is true float m_lifeTime ; // How long the particle will last // These two values are only take effect when m_lifeTime=-1 float m_lifeTimeMin ; // Minimum life time float m_lifeTimeMax ; // Maximum life time FLOAT m_age ; // Time since the particle was born, if age > lifeTime, particle was dead DWORD m_Color ; // Particle color D3DXVECTOR3 m_position ; // Current position D3DXVECTOR3 m_velocity ; // Current velocity D3DXVECTOR3 m_initVelocity ; // Initial velocity };
有了粒子类,我们还需定义一个粒子系统类,粒子系统类的任务是操作粒子类,来完成粒子的生成,更新,渲染,消亡及再生成的过程,通过生成及更新粒子的状态来实现整个粒子系统的效果。这是一个抽象类,包含四个纯虚函数,Init函数用来初始化粒子系统,比如创建Vertex Buffer,载入粒子对应的纹理等。Update函数用来更新粒子系统的状态,也就是更新系统中每个粒子的状态,包括粒子的速度,位置,存活时间等,都由该函数来更新。Render函数用来渲染粒子系统,这是最关键的一个函数。AddParticle函数用来添加新的粒子到粒子系统中,因为每个粒子都是有生命周期的,超过生命周期的粒子则为死忙状态,为了节约资源,我们将死亡状态的粒子重新设置为新生粒子,然后修改其属性再加入到粒子系统中,这样就可以通过有限的粒子实现多个爆炸效果了。
#include "Particle.h" class ParticleSystem { public: ParticleSystem(void); virtual ~ParticleSystem(void); public: virtual void Init() = 0 ; virtual void Update(float timeDelta) = 0 ; virtual void Render() = 0 ; virtual void AddParticle() = 0 ; virtual void ResetParticle(Particle* particle) = 0 ; }; #endif // end PARTICLESYSTEM_H
然后,我们定义一个Emitter类来继承上面的抽象类,并实现其中每个纯虚函数。Emitter类来完成具体的粒子系统需要的工作。
先看构造函数,构造函数主要做一些初始化工作。
Emitter::Emitter(IDirect3DDevice9* pDevice, EmitterConfig* emitterConfig, ParticleConfig* particleConfig) :m_particleTexture(NULL), pVB(NULL) { vbOffset = 0 ; // vertex buffer offset vbBatchSize = 50 ; // number of particles to render every time device = pDevice ; // D3D device m_position = emitterConfig->Position ; m_numparticlestoadd = emitterConfig->NumParticlestoAdd ; m_maxNumParticles = emitterConfig->MaxNumParticles ; vbSize = m_maxNumParticles ; // Vertex buffer size // Particle attributes m_particleColor = particleConfig->Color ; m_particleTexName = particleConfig->TextureName ; m_particleLifeTime = particleConfig->LifeTime ; }
然后是Init函数,在这个函数里,我们创建Vertex Buffer,并从文件创建粒子纹理。
void Emitter::Init() { // Create vertex buffer device->CreateVertexBuffer( vbSize * sizeof(POINTVERTEX), D3DUSAGE_DYNAMIC | D3DUSAGE_POINTS | D3DUSAGE_WRITEONLY, D3DFVF_POINTVERTEX, D3DPOOL_DEFAULT, // D3DPOOL_MANAGED can't be used with D3DUSAGE_DYNAMIC &pVB, 0); std::string resourcePath = "../Media/" ; resourcePath += m_particleTexName ; // Create texture D3DXCreateTextureFromFile(device, resourcePath.c_str(), &m_particleTexture) ; }
接下来是Update函数,注意这个函数每一帧都会调用一次,而且先于Render函数调用,所以整个粒子系统在渲染之前是通过该函数对每个粒子进行初始状态设置的。在这个函数中,我们对容器vector<Particle>中的每个粒子都进行状态更新。首先判断粒子是否存活,如果存活则更新状态,否则通过调用ResetParticle函数重置粒子状态为存活,并再次将其加入粒子系统,这样可以避免生成新的粒子,在性能上可以获得优势。更新粒子的状态包括更新位置,更新粒子时间及颜色,如果粒子的存活时间超过其生命周期则将其置为死亡状态。第二个for语句用来生成新的粒子,因为刚开始,整个粒子系统中并没有粒子存在,所以容器vector<Particle>为空,这意味着第一个for语句在粒子系统刚刚运行时是不会执行的。需要通过第二个for语句向系统中增加新的粒子。直到填满整个容器。随后的循环渲染才会调用第一个for语句。
void Emitter::Update(float timeDelta) { for (std::vector<Particle>::iterator citor = buffer.begin(); citor != buffer.end(); ++citor) { if (citor->m_isLive) // Only update live particles { citor->m_position += timeDelta * citor->m_velocity * 10.0f; citor->m_age += timeDelta ; citor->m_Color = this->m_particleColor ; if (citor->m_age > citor->m_lifeTime) { citor->m_isLive = false ; } } else ResetParticle((Particle*)(&(*citor))) ; } // Emit new particle for (int i = 0 ; i < m_numparticlestoadd && buffer.size() < vbSize; ++i) { Particle particle ; ResetParticle(&particle) ; buffer.push_back(particle) ; } }
再下来就是ResetParticle函数,这个函数用来重置一个死亡粒子的状态,使其再次进入粒子系统,这样要比重新生成一个粒子节省时间。首先将粒子状态置为存活,然后将其已存活时间设置为0,然后是设置粒子的生命周期,这里可以从配置文件读取值,也可以通过函数随机生成一个值,这样的话每个粒子的生命周期都是随机的,会产生不同的效果。否则的话,所有粒子同时生成,同时消亡,则略显生硬。接下来设置粒子的位置和颜色,最后设置粒子的速度,这里的速度和物理学中的速度一样,是个矢量,我们选取范围为[-1,-1,-1]到[1,1,1]内的矢量,这样产生的所有速度将构成一个半径为1的球体。和烟花的效果比较类似。
void Emitter::ResetParticle(Particle* particle) { particle->m_isLive = true ; particle->m_age = 0.0f ; if (m_particleLifeTime != -1) { particle->m_lifeTime = m_particleLifeTime ; } else { particle->m_lifeTime = Utilities::GetRandomFloat(0, 1) ; } particle->m_position = m_position ; particle->m_Color = m_particleColor ; D3DXVECTOR3 min = D3DXVECTOR3(-1.0f, -1.0f, -1.0f); D3DXVECTOR3 max = D3DXVECTOR3( 1.0f, 1.0f, 1.0f); Utilities::GetRandomVector(&particle->m_velocity, &min, &max); // normalize to make spherical D3DXVec3Normalize(&particle->m_velocity, &particle->m_velocity); }
接下来是AddParticle函数,该函数首先生成并重置一个粒子,然后将该粒子添加到粒子系统中。
void Emitter::AddParticle() { Particle particle ; ResetParticle(&particle) ; buffer.push_back(particle) ; }
最后,也是最重要的,Render函数,用来完成最终的粒子系统渲染工作。在这个函数里,我们首先设置一系列的RenderState,这些RenderState都是用来设置粒子的渲染状态,这里就不一一详述了,接下来设置纹理,也不必多说。最后是真正绘制粒子的代码,在绘制的时候,我们采用分批处理的办法,每次绘制一部分粒子,这里我们设置了一个vbBatchSize值,这就是每次绘制的粒子个数,我们会在Vertex Buffer中一次性锁住这么多粒子,然后绘制,绘制完毕移动到下一批,继续绘制,直到剩下的粒子数小于vbBatchSize,最后再把所有剩下的粒子一次性绘制完毕即可。
void Emitter::Render() { // Set render state device->SetRenderState( D3DRS_ALPHABLENDENABLE, TRUE ); device->SetRenderState( D3DRS_SRCBLEND, D3DBLEND_ONE ); device->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_ONE ); device->SetRenderState( D3DRS_POINTSPRITEENABLE, TRUE) ; device->SetRenderState( D3DRS_POINTSCALEENABLE, TRUE) ; device->SetRenderState( D3DRS_POINTSIZE, Utilities::FloatToDword(0.5f) ); device->SetRenderState( D3DRS_POINTSIZE_MIN, Utilities::FloatToDword(0.00f) ); device->SetRenderState( D3DRS_POINTSCALE_A, Utilities::FloatToDword(0.00f) ); device->SetRenderState( D3DRS_POINTSCALE_B, Utilities::FloatToDword(0.00f) ); device->SetRenderState( D3DRS_POINTSCALE_C, Utilities::FloatToDword(1.00f) ); // Set texture device->SetTexture(0, m_particleTexture) ; device->SetStreamSource( 0, pVB, 0, sizeof(POINTVERTEX)); device->SetFVF(D3DFVF_POINTVERTEX) ; // Start at beginning if we reach the end of vb if(vbOffset >= vbSize) vbOffset = 0 ; POINTVERTEX* v ; pVB->Lock( vbOffset * sizeof(POINTVERTEX), vbBatchSize * sizeof(POINTVERTEX), (void**)&v, vbOffset ? D3DLOCK_NOOVERWRITE : D3DLOCK_DISCARD ) ; DWORD numParticleinBatch = 0 ; for (std::vector<Particle>::iterator citor = buffer.begin(); citor != buffer.end(); ++citor) { if (citor->m_isLive) // Only draw live particles { v->pos = citor->m_position ; v->color = citor->m_Color ; v++ ; numParticleinBatch++ ; if (numParticleinBatch == vbBatchSize) { pVB->Unlock() ; device->DrawPrimitive( D3DPT_POINTLIST, vbOffset, vbBatchSize) ; vbOffset += vbBatchSize ; if (vbOffset >= vbSize) vbOffset = 0 ; pVB->Lock( vbOffset * sizeof(POINTVERTEX), vbBatchSize * sizeof(POINTVERTEX), (void**)&v, vbOffset ? D3DLOCK_NOOVERWRITE : D3DLOCK_DISCARD ) ; numParticleinBatch = 0 ; } } } pVB->Unlock() ; // Render the left particles if (numParticleinBatch) { device->DrawPrimitive( D3DPT_POINTLIST, vbOffset, numParticleinBatch ) ; } // Restore state device->SetRenderState( D3DRS_ALPHABLENDENABLE, FALSE ); }
好啦,这就是整个粒子系统的绘制过程了,在这里我们采用了配置文件的方式,为的是能将每个粒子系统的参数写到文件了,这样每个文件实际上就对应一个特效,我们可以通过添加配置文件的方式来实现不同的特效,核心代码部分则不用修改。
点击这里下载程序可以查看动态效果。
Happy Coding!!!