第十四章
显示粒子时,用点图元(由D3DPRIMITIVETYPE类型的D3DPT_POINTLIST枚举常量表示)是一个很好的选择,但是,光栅化时,点图元将被映射为一个单个像素,这就无法提供很大的灵活性,因为实际应用中可能需要各种尺寸的粒子甚至希望能够对这些粒子进行纹理映射,在Direct3D 8.0之前,要想摆脱点图元的这个限制,只能是不去使用它,那时,程序员都愿意用广告牌技术来显示一个粒子,广告牌就是一个四边形,通过对其自身世界变换矩阵的控制,使其总是面向摄像机
Direct3D 8.0引入一种特别的点图元--点精灵(Point Sprite),该图元极适合用于粒子系统中,与普通的点图元不同,点精灵可进行纹理映射且其尺寸可变,点精灵不同于广告牌,描述点精灵时,仅需要一个单点(single point)即可。由于只需要存储和处理一个顶点而非4个(广告牌要用4个顶点描述),这就节省了内存和宝贵的运算时间
该结构描述粒子的位置和颜色
struct Particle
{
D3DXVECTOR3 _position;
D3DCOLOR _color;
static const DWORD FVF;
};
const DWORD Particle::FVF = D3DFVF_XYZ | D3DFVF_DIFFUSE|D3DFVF_PSIZE;
请注意,即使硬件不支持D3DFVFCAPS_PSIZE,借助像素着色器(vertex shader)也有可能控制每个粒子的尺寸
点精灵的行为很大程度上是由绘制状态来控制的
#D3DRS_POINTSPRITEENABLE 一个布尔值,默认为false
*若指定为true,则规定整个当前纹理被映射到点精灵上
*若指定为false,则规定点精灵(如果其顶点结构中含纹理坐标的话)的纹理坐标所指定的纹理元应被映射到点精灵上
#D3DRS_POINTSCALEENABLE一个布尔值,默认为false
*若至指定为true,则规定点的尺寸将用观察坐标系的单位来度量,观察坐标系的单位是仅用来描述摄像机坐标系中的3D点。点精灵的尺寸将依据近大远小的原则进行相应的比例变换
*若为false,则规定的尺寸将用屏幕坐标系的单位(即像素)来度量,如果你将该绘制状态指定为false,而且你想将点精灵的尺寸设为3,则点精灵将变为屏幕上一个3*3的像素区域.
_device->SetRenderState(D3DRS_POINTSCALLENABLE,true);
#D3DRS_POINTSIZE 指定点精灵的尺寸,该值可被解释为观察坐标系中的点精灵尺寸,也可被解释为屏幕坐标系中点精灵尺寸,这主要取决于绘制状态D3DRS_POINTSCALEENABLE的设置下面的代码将点尺寸设为2.5个单位
_device->SetRenderState(D3DRS_POINTSIZE,d3d::FtoDw(2.5f));
d3d::FtoDW函数,其功能是将float型变量强制转为DWORD类型而且必须这样做,因为IDirect3DDevice9::SetRenderState函数要求传入DWORD类型值而非float型
DWORD d3d::FtoDw(float f)
{
return *((DWORD*)&f);
}
#D3DRS_POINTSIZE_MIN指定点精灵可取的最小尺寸
例:将点的尺寸的最小值设为0.2
_device->SetRenderState(D3DRS_POINTSIZE_MIN,d3d::FtoDw(0.2f));
#D3DRS_POINTSIZE_MAX指定点精灵可取的最大尺寸
例:将点的尺寸的最大值设为5.0
_device->SetRenderState(D3DRS_POINTSIZE_MAX,d3d::FtoDw(5.0f));
#D3DRS_POINTSCALE_A,D3DRS_POINTSCALE_B,D3DRS_POINTSCALE_C这3个常量控制了点精灵的尺寸如何随距离发生变化,这里的距离是指点精灵到摄像机的距离
给定距离和这些常量时,Direct3D使用如下公式计算点精灵的最终尺寸:
FinalSize = ViewportHeight * size* squrt(1/A + B(D)+ C(D*D))
#FinalSize计算出距离后,点精灵的最终尺寸
#ViewportHeight视口(viewport)高度
#Size对应于绘制状态D3DRS_POINT_SIZE所指定的值
#A,B,C分别对应于绘制状态D3DRS_POINTSCALE_A,D3DRS_POINTSCALE_B和D3DRS_POINTSCALE_C所指定的值
#D在观察坐标系中点精灵到摄像机的距离.由于在观察坐标系中,摄像机位于坐标原点,所以D=squrt(x*x + y * y + z * z),其中(x,y,z)是点精灵在观察坐标系中的位置
例:对点精灵的距离常量进行了设置,这样点精灵就随着距离的增大而变小
_device->SetRenderState(D3DRS_POINTSCALE_A,d3d::FtoDw(0.0f));
_device->SetRenderState(D3DRS_POINTSCALE_B,d3d::FtoDw(0.0f));
_device->SetRenderState(D3DRS_POINTSCALE_C,d3d::FtoDw(1.0f));
粒子除了位置和颜色外往往还具有许多其他的属性,例如:粒子可具有一定的速度,但是绘制粒子时并不需要这些附加属性,所以,将用于绘制粒子的数据与粒子的属性分别存储在两个不同的结构中,当要创建,销毁或更新粒子时,需要涉及粒子的属性,当准备绘制粒子时,可将粒子的位置和颜色信息复制到Particle结构中
粒子的属性与所要模拟的粒子系统的特定类型息息相关。通过指定一些常用的属性可以使这些属性结构变得通用一些
下面是一个较通用的粒子属性结构
struct Attribute
{
D3DXVECTOR3 _position;#粒子在世界坐标系中的位置
D3DXVECTOR3 _velocity;#粒子速度,度量单位常用单位/秒
D3DXVECTOR3 _acceleration;#粒子加速度 度量单位常用单位/秒*秒
float _lifeTime;#粒子自诞生到消亡所需的时间,例如,可能在一段时间后,杀死一段光束粒子
float _age;#粒子当前年龄
D3DXCOLOR _color;#粒子颜色
D3DXCOLOR _colorFade; #粒子颜色随时间渐弱
bool _isAlive;#为true,表面粒子处于活动状态,否则粒子处于死亡状态
};
粒子系统(Particle System)是众多粒子的集合,并负责对这些粒子进行维护和显示。粒子系统跟踪系统中影响所有粒子状态的全局属性,例如粒子的尺寸,粒子源,将要映射到粒子的纹理等,按照功能来说,粒子系统主要负责更新(updating),显示(displaying),杀死(kill)以及创建(creating)粒子
可归纳并找到所有粒子系统都需要的基本属性,我们将这些通用的属性封装在一个抽象基类--PSystem类中,该类将作为所要实现的全部具体的粒子系统的父类
class PSystem
{
public:
PSystem();
virtual ~PSystem();
//该方法完成一些Direct3D设备相关的初始化工作,如创建顶点缓存以存储点精灵,创建纹理等
//顶点缓存的创建过程包含了一些标记
virtual bool init(IDirect3DDevice9* device,char* texFileName);
virtual void reset();
// sometimes we don't want to free the momory of a dead particle
// but rather respawn it instead
virtual void resetParticle(Attribute* attribute) = 0 ;//该方法重新设定粒子的属性值,粒子的属性应如何重新设置依赖于特定粒子系统的细节,所以该方法声明为抽象函数,并强制子类必须实现该函数
virtual void addParticle();
virtual void updata(flat timeDelta) = 0 ;//该方法用于对系统中的所有粒子进行更新,由于该方法的实现依赖于特定粒子系统的细节,我们将该方法声明为抽象方法,并强制子类必须实现该方法
virtual void preRender();
virtual void render();//该方法用于显示系统中的所有粒子,该方法的实现相当复杂
virtual void postRender();
bool isEmpty();//当前系统中没有粒子,该函数返回true,反之返回false
bool isDead();//系统中所有粒子均已死亡,该函数返回true,反之返回false
protected:
virtual void removeDeadParticles();
protected:
IDirect3DDevice9* _deivce;
D3DXVECTOR3 _origin;// 系统的粒子源,所有的粒子都将从系统粒子源产生
d3d::BoundingBox _boundingBox;//限制粒子的活动范围的外接体
float _emitRate;// rate new particles are added to system 新增粒子的增加率,该值用粒子数/秒
float _size;// size of particles系统所有粒子的尺寸
IDirect3DTexture9* _tex;
IDirect3DVertexBuffer9* _vb;
std::list<Attribute> _particles;//系统中粒子的属性列表,可用该列表对粒子进行创建,销毁和更新
int _maxParticles;//max allowed partices system can have 在某个时间,系统所允许的最大粒子数
// Following three data elements used for rendering the p-system efficiently
DWORD _vbSize; // size of vb 在给定时间顶点缓存中所存储的顶点个数,该值不依赖于粒子系统中的实际粒子个数
DWORD _vbOffset;// offset in vb to lock 该变量子顶点缓存首地址算起的偏移量,标识了下一批粒子将从顶点缓存的何处开始复制,例,第一批粒子存储在顶点缓存0-499项,则下一批粒子在顶点缓存中开始复制的偏移量将为500
DWORD _vbBatchSize ;// number of vertices to lock starting at _vbOffset 每批粒子的数目
};
hr = device->CreateVertexBuffer(
_vbSize * sizeof(Particle),
D3DUSAGE_DYNAMIC | D3DUSAGE_POINTS | D3DUSAGE_WRITEONLY,
Particle::FVF,
D3DPOOL_DEFAULT,//D3DPOOL_MANAGED can't be used with D3DUSAGE_DYNAMIC
&_vb,
0);
注意,这里使用了动态缓存,这是因为需要在每帧中对粒子进行更新,这就意味着需要访问顶点缓存的存储区
注意,使用了标记D3DUSAGE_POINTS,该标记指定了顶点缓存将用于存储点精灵
注意,顶点缓存的尺寸已有变量_vbSize预先定义好,该值与系统中的粒子个数无关,即,_vbSize基本不可能与系统的粒子数相等,这是因为绘制粒子系统时往往是分批绘制的,而非一蹴而就
使用的是默认的内存池而非常用的托管内存池,这是因为动态顶点缓存不允许被放置在托管内存池中
//该方法重新设定系统中每个粒子的属性
void PSystem::reset()
{
std::list<Attribute>::iterator i;
for(i = _particles.begin(); i != _particles.end();i++)
{
resetParticle(&(*i));
}
}
addParticle该方法为系统增加一个粒子,该方法在将粒子加入列表之前,首先调用 resetParticle方法对粒子进行初始化
void PSystem::addParticle()
{
Attribute attribute;
resetParticle(&attribute);
_particles.push_back(attribute);
}
preRender用于在绘制之前,对那些必须设置的初始绘制状态进行设置,由于该方法的实现也依赖于具体的系统,于是将其声明为虚函数
void PSystem::preRender()
{
_device -> SetRenderState(D3DRS_LIGHTING,false);
_device -> SetRenderState(D3DRS_POINTSPRITEENABLE,true);
_device -> SetRenderState(D3DRS_POINTSCALEENABLE,true);
_device -> SetRenderState(D3DRS_POINTSIZE,d3d::FtoDw(_size));
_device -> SetRenderState(D3DRS_POINTSIZE_MIN,d3d::FtoDw(0.0f));
// control the size of the particle relative to distance
_device -> SetRenderState(D3DRS_POINTSCALE_A,d3d::FtoDw(0.0f));
_device -> SetRenderState(D3DRS_POINTSCALE_B,d3d::FtoDw(0.0f));
_device -> SetRenderState(D3DRS_POINTSCALE_C,d3d::FtoDw(1.0f));
//use alpha from texture
_device -> SetTextureStageState(0,D3DTSS_ALPHAARG1,D3DTA_TEXTURE);
_device -> SetTextureStageState(0,D3DTSS_ALPHAOP,D3DTOP_SELECTARG1);
_device -> SetRenderState(D3DRS_ALPHABLENDENABLE,true);
_device -> SetRenderState(D3DRS_SRCBLEND,D3DBLEND_SRCALPHA);
_device -> SetRenderState(D3DRS_DESTBLEND,D3DBLEND_INVSRCALPHA);
}
注意,启用了Alpha融合,这样当前纹理的Alpha通道就指定了纹理像素的透明度,以此来产生各种效果,一种常用的场合就是获取像纹理一样的非矩形的粒子,例:要获取一个圆形的像‘雪球’一样的粒子,可使用一个具有Alpha通道(黑色背景中有白色圆)的纯白色纹理,这样只有白色的圆形会显示出来,矩形的白色纹理就得不到显示
postRender该方法存储一个特定粒子系统可能已设置好的任何绘制状态,由于这也依赖于具体的系统,将该函数声明为虚函数
void PSystem::postRender()
{
_device->SetRenderState(D3DRS_LIGHTING,true);
_device->SetRenderState(D3DRS_POINTSPRITEENABLE,false);
_device->SetRenderState(D3DRS_POINTSCALEENABLE,false);
_device->SetRenderState(D3DRS_ALPHABLENDENABLE,false);
}
removeDeadParticles 对属性列表_Particle进行搜索,并从列表中移除任何已死亡的粒子
void PSystem::removeDeadParticles()
{
std::list<Attribute>::interator i;
i = _particles.begin();
while(i != _particles.end())
{
if(i->isAlive == false)
{
// erase returns the next iterator ,so no need to
// increment to the next one ourslves
i = _particles.erase(i);
}
else
{
i++;// next in list
}
}
}
注意,该方法通常在子类的update方法中被调用,已移除任何已被杀死的粒子,但是,对于某些粒子系统,回收那些处于死亡状态的粒子比销毁它们有更多的优势,在粒子诞生和消亡时,不是从列表中为其重新分配内存或回收内存,只是对那些处于死亡状态的粒子重新设置,使其变为新粒子
绘制粒子系统:创建一个容量合理的顶点缓存,然后将该顶点缓存划分为若干片段,然后创建全局变量i= 0 在跟踪当前片段
对每帧的操作如下:
1 更新所有粒子
2 将全部活动粒子被绘制
#若顶点缓存未满,则
(1)用标记 D3DLOCK_NOOVERWRITE锁定片段i;
(2)将500个粒子复制到锁定片段i中
#若顶点缓存已存满,则
(1)用自顶点缓存的起始位置开始i = 0
(2)将用标记D3DLOCK_DISCARD锁定片段i
(3)将500个粒子复制到锁定片段i中
#绘制片段i中的粒子
#下一片断:i++
注意: 前面顶点缓存指定为动态的,所以在锁定顶点缓存时,使用了动态锁定标记D3DLOCK_NOOVERWRITE和D3DLOCK_DISCARD,这些标记允许对顶点缓存中尚未被绘制的部分进行锁定,这样错丝毫不影响顶点缓存中其余部分的绘制,例如,假定要绘制片段0中的粒子,通过使用标记D3DLOCK_NOOVERWRITE,当绘制片段0中的粒子时,可对片段1锁定并填充,这就避免了可能出现绘制中断的情况
该方法效率更高,首先,减少了顶点缓存的容量,其次,cpu和图形卡现在可以协同工作,向顶点缓存复制(CPU来做)一小批粒子,然后再对该批粒子进行绘制(图形卡来做),接下来再为顶点缓存复制下一批粒子,然后对其进行绘制,该过程一直持续到全部粒子都被绘制完毕,从中可看出,在填充顶点缓存过程中,图形卡不在处于空闲状态
//列出绘制方法的实现代码
void PSystem::render()
{
if (!_particles.empty())
{
// set render states
preRender();
_device -> SetTexture(0,_tex);
_device -> SetFVF(Particle::FVF);
_device -> SetStreamSource(0,_vb,0,sizeof(Particle));
// start at beginning if we're at the end of the vb
if(_vbOffset >= _vbSize)
{
_vbOffset = 0;
Particle* v = 0;
_vb->Lock(_vbOffset * sizeof(Particle),
_vbBatchSize * sizeof(Particle),
(void**)&v,
_vbOffset ? D3DLOCK_NOOVERWRITE:D3DLOCK_DISCARD);
DWORD numParticlesInBatch = 0 ;
// Until all particles have been rendered
std::list<Attribute>::iterator i;
for(i = _particles.begin(); i != _particles.end();i++)
{
if(i->isVlive)
{
// copy a batch of the living particles to the
// next vertex buffer segment
v->_position = i -> _position;
v->_color = (D3DCOLOR)i->_color;
v++; // element
numParticlesInBatch++;// increase batch counter
// if this batch full ?
if (numParticlesInBatch == _vbBatchSize)
{
// draw the last batch of particles that was
// copied to the vertex buffer
_vb->Unlock();
_device ->DrawPrimitive(
D3DPT_POINTLIST,
_vbOffset,
_vbBatchSize);
// While that batch is drawing ,start filling the
// next batch with particles
// move the offset to the start of the next batch
_vbOffset += _vbBatchSize;
// don't offset into momory thats outside the vb's range
// if we're at the end ,start at the begining
if(_vbOffset >= _vbSize)
_vbOffset = 0 ;
_vb->Lock(_vbOffset * sizeof(Particle),
_vbBatchSize * sizeof(Particle),
(void**)&v,
_vbOffset ? D3DLOCK_NOOVEWRITE : D3DLOCK_DISCARD);
numParticlesInBatch = 0 ;// reset for new batch
}
}
}
_vb->Unlock();
// its possible that the LAST batch being filled never
// got rendered because the condition
// (numParticlesInBatch == _vbBatchSize) would not have
// been satisfied. wo draw the last partially filled batch now
if (numParticlesInBatch)
{
_device->DrawPrimitive(
D3DPT_POINTLIST,
_vbOffset,
numParticlesInBatch);
}
// next block
_vbOffset += _vbBatchSize;
// reset render states
postRender();
}
}
}
粒子系统随机功能:
返回区间[lowBound,highBound]内随机浮点数
float d3d::GetRandomFloat(float lowBound,float highBound)
{
if (lowBound >= highBound)// bad input
return lowBound;
// get random float in [0,1] interval
float f = (rand()%10000) * 0.0001f;
// return float in [lowBound,highBound] interval
return (f*(highBound - lowBound)) + lowBound;
}
输出一个限制在由最小点min和最大点max确定的外接体中的随机向量
void d3d::GetRandomVector(
D3DXVECTOR3* out,
D3DXVECTOR3* min,
D3DXVECTOR3* max)
{
out->x = GetRandomFloat(min->x,max->x);
out->y = GetRandomFloat(min->y,max->y);
out->z = GetRandomFloat(min->z,max->z);
}