引言
上一篇講了如何在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!!!
