第十集 粒子系统的实现
第六集中的二维纹理技术有着天生的缺陷, 1. 纹理走样; 2. 表现单一. 特别是在表达不规则物体如流水, 行云时, 这些缺陷被放大了.
于是有了三维纹理技术, 三维纹理中的纹理空间和物体空间都是三维的. 如显示大理石雕象, 在物体空间是雕象模型, 在纹理空间是大理石的三维纹理, 把雕象模型放进纹理空间的特定地方, 雕象模型表面和纹理空间的交点就是雕象模型要显示的纹理.
那么怎样构造三维纹理? 常用的方式为,
(1). 离散采样. 高分辨率的纹理很耗内存.
(2). 数学解析. --- 过程纹理技术.
在过程纹理中, 有一种能模拟大量不同类型不规则物体的算法 – 粒子系统.
其实, 粒子系统这个名称非常的专业化, 实际我们可以用Windows编程中的画像素来理解,
a. 粒子 --- POINT;
保存要画的像素的坐标.
b. 粒子系统 --- many POINTs;
很多像素要画, 于是要管理好画的过程, 如用个循环, 从POINT array中读取数据, 再画相应的像素.
10.1 粒子系统的实现
在一个封闭的空间中, 成千上万的微小的, 随机不规则运动的粒子组成一个不断变化的不规则物体 – 这些粒子构成了一个封闭的系统, 定义为粒子系统.
粒子系统 == 显示N多随机点的算法, 这是我们要理解的.
在感慨天空中行云的壮阔时, 很少有人会直接指出 -- 那只是一堆水蒸气; 不过现在我们是要创造虚拟世界中的云彩, 所以要从云的基本构成物质开始分析, 现实中云的基本元素 -- 水蒸气, 虚拟世界中的云的基本元素 -- 粒子.
10.1.1 粒子
粒子实际是粒子系统的数据存储单位, 一般粒子包含的数据类型有,
a. 位置; 虽然粒子随时在运动, 但按一定时间片来观察, 粒子还是有位置的.
b. 速度; 既然有位置的变化, 当然有速度存在, 速度的方向性是随时变化的.
c. 生命; 终究, 还是要面对的, 无非是长短的差别.
d. 颜色; 物体的五颜六色是由粒子的颜色决定的.
class CParticle
{
public :
CParticle() { Init();}
~CParticle() { }
VOID Init();
public :
D3DVECTOR m_vPos;
FLOAT m_fVel[3];
FLOAT m_fLife;
D3DXCOLOR m_clr;
};
使用类的形式来封装粒子包含的数据只是个人代码风格问题,
VOID CParticle::Init()
{
m_vPos.x = randf(-16.0, 16.0);;
m_vPos.y = randf(-16.0, 16.0);;
m_vPos.z = 0.0;
m_fVel[0] = 0.0;
m_fVel[1] = 0.0;
m_fVel[2] = 0.0;
m_fLife = randf(8.0, 16.0);
m_clr.a = 1.0;
m_clr.r = randf(0.0, 1.0);
m_clr.g = randf(0.0, 1.0);
m_clr.b = randf(0.0, 1.0);
}
单个粒子是无法形成我们要模拟的不规则物体的, 粒子只是粒子系统中存储数据的单位.
10.1.2 基本粒子系统
粒子系统的作用是管理系统内所有数据(粒子)的初始化, 更新, 删除(不是真的删除), 显示.
class CPSystem
{
public :
CPSystem(LPDIRECT3DVERTEXBUFFER9 pD3DVBuffer);
~CPSystem();
HRESULT Init();
VOID Render();
private :
LPDIRECT3DDEVICE9 m_pD3DDev;
LPDIRECT3DVERTEXBUFFER9 m_pD3DVBuffer;
CParticle m_aP[PARTICLE_COUNT];
};
CPSystem::CPSystem(LPDIRECT3DVERTEXBUFFER9 pD3DVBuffer)
: m_pD3DVBuffer(pD3DVBuffer)
{
}
CPSystem::~CPSystem()
{
SAFERELEASE( m_pD3DDev );
}
HRESULT CPSystem::Init()
{
for (INT i = 0; i < PARTICLE_COUNT; i++)
{
m_aP[i].Init();
}
return m_pD3DVBuffer->GetDevice(&m_pD3DDev);
}
从上面这个类中, 我们明显看出它的作用就是初始化有PARTICLE_COUNT个成员的数组, 然后在渲染的时候根据这个数组的各成员的数据显示很多的点.
粒子系统的渲染过程
过程纹理是实时计算的, 粒子系统一帧画面的形成步骤,
(1). 生命周期内的粒子根据粒子的属性对粒子进行变换, 超出生命周期的粒子”删除”.
(2). 如系统内粒子数量低于某标准值, 初始化新粒子, 加入系统.
(3). 绘制所有在生命周期内的粒子组成的图形.
VOID CPSystem::Render()
{
INT nRand = 0;
UINT nSize = PARTICLE_COUNT * sizeof(MYVERTEX);
FLOAT fRand = 0.0;
FLOAT fVel = 0.0;
MYVERTEX* pV = NULL;
if(FAILED(m_pD3DVBuffer->Lock(0, nSize, (LPVOID*)(&pV), D3DLOCK_DISCARD)))
{
return;
}
// 将上次更新的数据copy到vertex buffer让Direct3D显示
for (INT i = 0; i < PARTICLE_COUNT; i += 4)
{
pV->v = m_aP[i].m_vPos;
pV->colour = DWORD(m_aP[i].m_clr);
pV++;
pV->v = m_aP[i+1].m_vPos;
pV->colour = DWORD(m_aP[i+1].m_clr);
pV++;
pV->v = m_aP[i+2].m_vPos;
pV->colour = DWORD(m_aP[i+2].m_clr);
pV++;
pV->v = m_aP[i+3].m_vPos;
pV->colour = DWORD(m_aP[i+3].m_clr);
pV++;
}
m_pD3DVBuffer->Unlock();
m_pD3DDev->SetStreamSource(0, m_pD3DVBuffer, 0, sizeof(MYVERTEX));
m_pD3DDev->SetFVF(D3DFVF_MYVERTEX);
m_pD3DDev->DrawPrimitive(D3DPT_POINTLIST, 0, PARTICLE_COUNT);
// 更新数据, 当上次中的生命数据< 0, 表示要将数据删除, 实际重新初始化了这个数据
for (INT i = 0; i < PARTICLE_COUNT; i++)
{
if (m_aP[i].m_fLife > 0.0)
{
nRand = randn(3);
fRand = randf(0.0f, 0.6f);
fVel = FLOAT(0.3f - fRand);
m_aP[i].m_fVel[nRand] += fVel;
m_aP[i].m_vPos.x += m_aP[i].m_fVel[0];
m_aP[i].m_vPos.y += m_aP[i].m_fVel[1];
m_aP[i].m_vPos.z += m_aP[i].m_fVel[2];
m_aP[i].m_fLife -= fRand;
}
else
{
m_aP[i].Init();
}
}
}
10.1.3 DirectX Graphics中的Point Sprite
上面的代码只能显示五颜六色的点, 没有大小区别, DirectX Graphics中存在控制标志, 使得显示的点可以根据离视点的远近自动调整最佳的显示大小, 这时的点有个好听的名字 -- Point Sprite.
这些标志统一由IDirect3DDevice9中的函数SetRenderState来控制,
(1). D3DRS_POINTSPRITEENABLE
(2). D3DRS_POINTSCALEENABLE
(3). D3DRS_POINTSIZE
(4). D3DRS_POINTSIZE_MIN
(5). D3DRS_POINTSIZE_MAX
(6). D3DRS_POINTSCALE_A
(7). D3DRS_POINTSCALE_B
(8). D3DRS_POINTSCALE_C
各个参数的作用DirectX9c SDK有详细的描述.
在调用DrawPrimitive画点之前, 我们打开这些标志, 告诉DirectX Graphics要画的点是Point Sprite, 当完成绘制以后, 一般关闭标志, 防止对另外的渲染过程有影响
// open point sprite flags
EnterPSprite();
m_pD3DDev->DrawPrimitive(D3DPT_POINTLIST, 0, PARTICLE_COUNT);
ExitPSprite(); // close flags
VOID CPSystem:: EnterPSprite()
{
m_pD3DDev->SetRenderState(D3DRS_POINTSPRITEENABLE, TRUE);
m_pD3DDev->SetRenderState(D3DRS_POINTSCALEENABLE, TRUE);
m_pD3DDev->SetRenderState(D3DRS_POINTSIZE, FTOD(2.0f));
m_pD3DDev->SetRenderState(D3DRS_POINTSIZE_MIN, FTOD(1.0f));
m_pD3DDev->SetRenderState(D3DRS_POINTSCALE_A, FTOD(0.0f));
m_pD3DDev->SetRenderState(D3DRS_POINTSCALE_B, FTOD(0.0f));
m_pD3DDev->SetRenderState(D3DRS_POINTSCALE_C, FTOD(1.0f));
}
VOID CPSystem:: ExitPSprite()
{
// 只要关闭这两个就可以了, 其他的会自动失效
m_pD3DDev->SetRenderState(D3DRS_POINTSPRITEENABLE, FALSE);
m_pD3DDev->SetRenderState(D3DRS_POINTSCALEENABLE, FALSE);
}
再看看显示的结果, 好看多了, 但同时也发现放大的点是正方形的, 没有”粒子”的感觉. 一般”粒子”总认为是圆形的 --- 习惯性思维.
要将正方形变成圆形的, 就需要给粒子加上纹理, 粒子正方形就会有纹理显示,这里用到了Alpha混合, Alpha混合要到以后再解释, 这里我们让它先客串一下.
m_pD3DDev->SetTexture(0, m_pTexture); // 设置纹理
m_pD3DDev->SetStreamSource(0, m_pD3DVBuffer, 0, sizeof(MYVERTEX));
m_pD3DDev->SetFVF(D3DFVF_MYVERTEX);
EnterPSprite();
m_pD3DDev->DrawPrimitive(D3DPT_POINTLIST, 0, PARTICLE_COUNT);
ExitPSprite();
VOID CPSystem::EnterPSprite()
{
// alpha通道参数设置
m_pD3DDev->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
m_pD3DDev->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_ONE);
m_pD3DDev->SetRenderState(D3DRS_POINTSPRITEENABLE, TRUE);
m_pD3DDev->SetRenderState(D3DRS_POINTSCALEENABLE, TRUE);
m_pD3DDev->SetRenderState(D3DRS_POINTSIZE, FTOD(2.0f));
m_pD3DDev->SetRenderState(D3DRS_POINTSIZE_MIN, FTOD(1.0f));
m_pD3DDev->SetRenderState(D3DRS_POINTSCALE_A, FTOD(0.0f));
m_pD3DDev->SetRenderState(D3DRS_POINTSCALE_B, FTOD(0.0f));
m_pD3DDev->SetRenderState(D3DRS_POINTSCALE_C, FTOD(1.0f));
}
VOID CPSystem::ExitPSprite()
{
// 关闭alpha通道
m_pD3DDev->SetRenderState(D3DRS_ALPHABLENDENABLE, FALSE);
m_pD3DDev->SetRenderState(D3DRS_POINTSPRITEENABLE, FALSE);
m_pD3DDev->SetRenderState(D3DRS_POINTSCALEENABLE, FALSE);
}
10.2 粒子系统的例子
10.2.1 例子说明
这是一个不模仿物体的粒子系统的例子, 由这个例子变化到模拟不规则物体的粒子系统只是各粒子的初始化值不同, 更新时的数据加减…不同而已, 这些网上的资源很多, 试着改数据模拟一下.
第十集 小结
粒子系统是根据粒子中的数据显示随机点的过程, 结合DirectX Graphics中的Point Sprite标志, 可以模拟很多现实中的不规则物体.