之前第一次学习的时候写过两次粒子系统,但都是只能用于特定的粒子效果,类的扩展性较差。刚刚又把粒子系统重新规划了一下。
首先,粒子要有诸多属性,我们用一个结构体表示;然后在粒子系统类中创建一个粒子容器;最后通过对容器的访问实现粒子属性的更新和渲染以及销毁。写完这个基类,以后只需从该基类派生,然后实现一下基类中的纯虚函数InitParticle(Particle& _out)即可。
//粒子结构体
struct Particle
{
D3DXVECTOR3 initPos; //初始位置
D3DXVECTOR3 initVel; //初始速度
float initSize; //初始尺寸
float initTime; //初始时间
float lifeTime; //粒子寿命
float mass; //粒子质量
D3DXCOLOR initColor; //初始颜色
static IDirect3DVertexDeclaration9* Decl;
};
//粒子系统类
class ParticleSystem
{
public:
ParticleSystem(LPDIRECT3DDEVICE9 _pd3dDevice,const char* _fxName, const char* _techName, const char* _textureName,
const D3DXVECTOR3& _acceleration,
const AABB& _box,
int _maxParticlesNum,
float _timePerParticle);
virtual ~ParticleSystem();
void AddParticle();
void SetPosition(const D3DXVECTOR3& _pos);
void SetBox(const AABB& _box);
void SetBoxVisiable(bool _visiable){m_BoundingBoxVisiable = _visiable;}
int GetAliveNum(){return m_AliveParticles.size();}
D3DXVECTOR3 GetBoxMin(){return m_BoxMin;}
D3DXVECTOR3 GetBoxMax(){return m_BoxMax;}
virtual void InitParticle( Particle& _out)=0;
virtual void Update(float _deltaTime);
virtual void Render();
virtual void Lost();
virtual void Reset();
virtual void Destroy();
protected:
virtual void RenderBoundingBox(const AABB& _box);
LPDIRECT3DDEVICE9 m_pd3dDevice;
ID3DXEffect* m_Effect; //效果指针
LPDIRECT3DTEXTURE9 m_Texture; //粒子纹理
LPDIRECT3DVERTEXBUFFER9 m_VertexBuffer; //顶点缓存
LPDIRECT3DVERTEXBUFFER9 m_BoxVertexBuffer; //包围盒顶点缓冲
D3DXMATRIX m_World; //世界矩阵
D3DXMATRIX m_InvWorld; //世界逆矩阵
float m_Time; //相对于第一个粒子出生的本地时间
D3DXVECTOR3 m_Acceleration; //粒子加速度
AABB m_Box; //整个粒子系统的包围盒
int m_MaxParticlesNum; // 本粒子系统最大的粒子数
float m_TimePerParticle; //每两个粒子出生的最小间隔时间
vector<Particle> m_Particles; //存放粒子对象实例的容器
vector<Particle*> m_AliveParticles; //存放有生命的粒子对象地址的容器
vector<Particle*> m_DeadParticles; //存放已死亡的粒子对象地址的容器
};
一个粒子有两个状态:存活态,死亡态。 有人会想,当粒子死亡时就把这个particle从容器中pop弹出,粒子要重生时只需再声明一个变量然后push压入容器中即可。这样一来当有大量的粒子不断重生和死亡时,可向而知,内存会发生怎样的高频变化,其实这是完全没有必要的。
换一种思路,一开始将粒子容器预留出最大限量的空间,此时列表中已经充满了粒子,每次刷新的时候我们遍历这个容器,通过粒子的年龄来判断是否死亡,如果死亡,则把该粒子的指针push到另外一个名叫 deadparticle 的容器中;如果粒子仍然处于存活态,则把其指针push到一个名叫 aliveparticle的容器中。 再看上面类的最后三个成员变量就是又来管理 两种不同态 的粒子的。
注意AliveParticles和DeadParticles存放的是粒子指针,这样效率高。
另一个需要优化的地方就是,在动态变化大量粒子的尺寸时,我们往往是根据粒子到眼睛的距离来变化的。这时我们就需要眼睛的位置和粒子的位置在同一个坐标空间中。当然是只需把眼睛位置转换到粒子的本地空间中最好。
在一个需要优化的地方便是要对粒子系统进行视锥体剪裁。也许大量的雪花剪裁没有意义,因为雪花总是在摄像机的前方被渲染。如果是一堆火,好几堆火酒十分有必要进行剪裁。办法当然最简单的就是加 包围盒。然后来一个包围盒与视锥体的 相交检测。这个检测的代码我看了一些相关的图形知识才看懂了,现贴上来供大家参考:
//6代表了视锥体的六个面,4代表了每个面的四个信息,见下面注释
float m_Frustum[6][4];
enum PlaneData
{
A = 0, // The X value of the plane's normal
B = 1, // The Y value of the plane's normal
C = 2, // The Z value of the plane's normal
D = 3 // The distance the plane is from the origin
};
//xyz为包围盒的最小点,x2y2z2为最大点
bool Frustum::BoxInFrustum( float x, float y, float z, float x2, float y2, float z2)
{
// Go through all of the corners of the box and check then again each plane
// in the frustum. If all of them are behind one of the planes, then it most
// like is not in the frustum.
for(int i = 0; i < 6; i++ )
{
if(m_Frustum[i][A] * x + m_Frustum[i][B] * y + m_Frustum[i][C] * z + m_Frustum[i][D] > 0) continue;
if(m_Frustum[i][A] * x2 + m_Frustum[i][B] * y + m_Frustum[i][C] * z + m_Frustum[i][D] > 0) continue;
if(m_Frustum[i][A] * x + m_Frustum[i][B] * y2 + m_Frustum[i][C] * z + m_Frustum[i][D] > 0) continue;
if(m_Frustum[i][A] * x2 + m_Frustum[i][B] * y2 + m_Frustum[i][C] * z + m_Frustum[i][D] > 0) continue;
if(m_Frustum[i][A] * x + m_Frustum[i][B] * y + m_Frustum[i][C] * z2 + m_Frustum[i][D] > 0) continue;
if(m_Frustum[i][A] * x2 + m_Frustum[i][B] * y + m_Frustum[i][C] * z2 + m_Frustum[i][D] > 0) continue;
if(m_Frustum[i][A] * x + m_Frustum[i][B] * y2 + m_Frustum[i][C] * z2 + m_Frustum[i][D] > 0) continue;
if(m_Frustum[i][A] * x2 + m_Frustum[i][B] * y2 + m_Frustum[i][C] * z2 + m_Frustum[i][D] > 0) continue;
// If we get here, it isn't in the frustum
return false;
}
// Return a true for the box being inside of the frustum
return true;
}
最后贴上今天下午写的一个类似传送点的粒子系统(最大粒子数2500):