粒子系统保存和操纵一种称为粒子的元素。粒子系统的职责就是对粒子施加不同的作用力,为它们增加一种真实的外观。粒子系统的类型包括爆炸、烟、火、火花等,而其他类型的效果可由许多小的元素构成。粒子是一个小物体,包含多种属性,如位置、速度(方向)和质量。游戏中,粒子通常是添加了纹理的小正方形,通常面向摄象机。在实现点状sprite时已经介绍过这一点。点状sprite可以由粒子系统使用,从而有效地生成多种不同的效果。粒子系统示例如图6.5所示。
通常,粒子相互之间独立作用,但却限制在粒子系统范围内。例如,所有的粒子也许从相同位置开始或从比较接近的位置开始,但它们都有自己的属性,将彼此分隔开。同样,通常在游戏中,粒子之间并不发生碰撞,通常那样会降低帧率。每个粒子可以有大量不同的属性,这包括前面提到的那些属性。此外,还有大小、能级、颜色、颜色增量(单位时间内颜色的变化量)等。对粒子系统的要求将确定系统中需要实现它的属性。多数粒子系统是通用系统,能处理不同类型的粒子。这是一种很有用的方法,但对规划和实现而言,这变得很复杂,对初学者更是如此。本章将介绍一个简单的粒子系统,它完全满足将要显示的效果。
脚本系统可以让粒子系统变得非常复杂。在粒子系统中使用脚本,可以方便地为游戏创建、修改和添加粒子系统,但付出的代价是增加了实现的复杂性。如果想要开发一个灵活完整的、可用于创建任意效果的通用粒子系统,那就值得付出额外的努力开发一个基于脚本的通用粒子系统。
本章将要创建的粒子系统是一种很简单的雨。本书将创建一个使用点状sprite、图像为雨滴纹理的系统,同时每个粒子还要在场景中下落,直到满足某个条件和粒子被销毁为止。实际上,粒子并未销毁,而是被重新初始化回到天空中而已,这样看上去雨像是连绵不断。雨粒子系统的完整演示程序可以在本书配套光盘的CHAPTER6文件夹中找到,名称为ParticleSystem。该演示程序由3个文件构成:main.cpp源文件、RainPS.h头文件以及RainPS.cpp源文件。
每个粒子的定义可以有多个属性。在简单的演示程序中只要知道粒子的位置和速度即可。雨滴从天空中的某个随机位置开始,该位置由粒子系统的位置、区域宽度、高度和深度确定。在生成一个新粒子时,它的位置随机分布在系统区域内,并随机赋给它一个速度。一旦完成初始化工作,粒子系统就会更新环境中所有粒子的位置。每一帧都是如此,更新工作持续到程序退出为止。
粒子系统每次将创建多达1000个新的粒子。系统可以存储的最大粒子数是3000。这里有两个类。一个针对粒子系统。粒子只需要跟踪雨滴的位置和速度即可。粒子系统将跟踪一列粒子,这些粒子的位置、最大的粒子数,当前正在使用的粒子总数,时间计数器,以及根据效果的宽度、高度和深度得到的系统效果区域。
粒子系统除了构造函数之外,还包括4个函数。这4个函数分别是Initialize()、Shutdown()、Update()和CreateParticle()。Initialize()函数初始化整个粒子系统,只需要在程序运行时调用一次。Shutdown()函数删除粒子链表,这样在使用完系统时就可以释放动态内存。Update()函数结尾处用于更新场景中所有现有粒子的位置。如果需要创建新粒子,那么在Update()函数结尾处可以生成新粒子。CreateParticle()函数根据发送给它的数量生成一批新粒子。CreateParticle()函数只是简单地将新生成的粒子添加到链表末端。一旦链表满了,那么就直到有一些粒子被销毁时才可以添加新粒子。如果粒子超出了粒子系统的高度,那么就销毁粒子。由于粒子系统要比产生粒子的位置低,因此一旦雨滴低于系统高度,就可以知道粒子被销毁了。粒子和粒子系统的声明如程序清单6.8所示。
RainPS.h
#ifndef _RAIN_PS_H_
#define _RAIN_PS_H_
// Max number of new particles.
#define UPDATE_AMT 1000
// 粒子
class CParticle
{
public:
CParticle()
{
m_pos[0] = 0; m_pos[1] = 0; m_pos[2] = 0;
m_vel[0] = 0; m_vel[1] = 0; m_vel[2] = 0;
}
// 粒子的位置
float m_pos[3];
// 粒子的速度
float m_vel[3];
};
// 粒子系统
class CRainPS
{
public:
// @vX, vY, vZ 赋值给float m_velocity[3]
// @dX, dY, dZ 赋值给float m_velDelta[3]
// 将vX,vZ,dX,dZ设置成0,使粒子只沿着Y轴运动,在x,z轴没有运动
CRainPS(float vX = 0, float vY = -4.0, float vZ = 0,
float dX = 0, float dY = 1.0, float dZ = 0);
// Init system.
// @x,y,z 赋值给float m_pos[3]
// @maxP 最大粒子数目
// @w,h,d 粒子系统效果区域的宽度、高度和深度
bool Initialize(float x = 0, float y = -3, float z = 0,
int maxP = 3000, float w = 2.0, float h = 3.0, float d = 2.0);
// Update all particles based on time.
// @scalar 时间参数
void Update(float scalar);
/*
CreateParticle()函数以函数准备创建的粒子总数为参数。该函数首先循环想要创建
的粒子数。对每个循环而言,都在链表末端增加一个新的粒子,并将粒子总数加1。
如果到了链表末端,就跳出循环,以避免超出数组边界。初始化新粒子时,要用区域中
的随机数设置粒子的位置和速度。区域是由粒子系统的位置以及效果的宽度、高度和深
度确定的。用0~1之间的随机数乘上面积就可以确保新粒子在该区域中。
*/
// 创建amount个粒子
// @amount 要创建的粒子的数目
void CreateParticle(int amount);
// Release all resources.
void Shutdown();
// Width, height, and depth of the area.
float m_width;
float m_height;
float m_depth;
// 用于初始化粒子位置的初始值
float m_pos[3];
// 存储粒子的列表list
CParticle *m_particles;
// 最大粒子数目
int m_maxParticles;
// 当前粒子数目
int m_particleCount;
// 时间计数器
float m_totalTime;
// 用于初始化粒子速度的初始值
float m_velocity[3];
// 用于初始化粒子速度的系数值
float m_velDelta[3];
};
#endif
RainPS.cpp
#include<math.h>
#include<stdlib.h>
#include"RainPS.h"
// 得到一个绝对值小于1的浮点数
#define GET_RANDOM ( ( (float)rand() - (float)rand() ) / RAND_MAX)
CRainPS::CRainPS(float vX, float vY, float vZ, float dX, float dY, float dZ)
{
m_width = 0; m_height = 0; m_depth = 0;
m_particles = 0; m_maxParticles = 0;
m_particleCount = 0; m_totalTime = 0;
m_pos[0] = m_pos[1] = m_pos[2] = 0;
m_velocity[0] = vX;
m_velocity[1] = vY;
m_velocity[2] = vZ;
m_velDelta[0] = dX;
m_velDelta[1] = dY;
m_velDelta[2] = dZ;
}
bool CRainPS::Initialize(float x, float y, float z, int maxP,
float w, float h, float d)
{
// Release all previous data.
Shutdown();
m_width = w; m_height = h; m_depth = d;
m_particles = NULL;
m_maxParticles = maxP;
m_particleCount = 0;
m_totalTime = 0;
m_pos[0] = x; m_pos[1] = y; m_pos[2] = z;
// Error checking.
if(m_maxParticles <= 0) m_maxParticles = 1;
// Allocate space for the particles.
m_particles = new CParticle[m_maxParticles];
if(!m_particles) return false;
return true;
}
void CRainPS::CreateParticle(int amount)
{
// Simply loop through and create the amount of particles.
for(int i = 0; i < amount; i++)
{
if(m_particleCount >= m_maxParticles) break;
// Create a particle at the next free slot.
m_particles[m_particleCount].m_pos[0] = m_pos[0] + GET_RANDOM * m_width;
m_particles[m_particleCount].m_pos[1] = m_height + GET_RANDOM * m_height;
m_particles[m_particleCount].m_pos[2] = m_pos[2] + GET_RANDOM * m_depth;
m_particles[m_particleCount].m_vel[0] = m_velocity[0] + GET_RANDOM * m_velDelta[0];
m_particles[m_particleCount].m_vel[1] = m_velocity[1] + GET_RANDOM * m_velDelta[1];
m_particles[m_particleCount].m_vel[2] = m_velocity[2] + GET_RANDOM * m_velDelta[2];
m_particleCount++;
}
}
void CRainPS::Update(float scalar)
{
int numParticles = 0;
// Loop through and update.
for(int i = 0; i < m_particleCount;)
{
m_particles[i].m_pos[0] += m_particles[i].m_vel[0] * scalar;
m_particles[i].m_pos[1] += m_particles[i].m_vel[1] * scalar;
m_particles[i].m_pos[2] += m_particles[i].m_vel[2] * scalar;
// If particle goes past min height, destroy.
// 因为雨滴的下落只改变Y值
if(m_particles[i].m_pos[1] <= m_pos[1])
{
m_particleCount--;
m_particles[i] = m_particles[m_particleCount];
}
else
i++;
}
// Calculate how many new particles we should create.
m_totalTime += scalar;
numParticles = (int)(UPDATE_AMT * m_totalTime);
if(m_totalTime > 1) m_totalTime = 0;
// Create them.
CreateParticle(numParticles);
}
void CRainPS::Shutdown()
{
if(m_particles)
delete[] m_particles;
m_particles = NULL;
m_particleCount = 0;
m_totalTime = 0;
}
main.cpp
#include<d3d9.h>
#include<d3dx9.h>
#include"RainPS.h"
#define WINDOW_CLASS "UGPDX"
#define WINDOW_NAME "Particle System"
#define WINDOW_WIDTH 640
#define WINDOW_HEIGHT 480
// Function Prototypes...
bool InitializeD3D(HWND hWnd, bool fullscreen);
bool InitializeObjects();
void RenderScene();
void Shutdown();
// Direct3D object and device.
LPDIRECT3D9 g_D3D = NULL;
LPDIRECT3DDEVICE9 g_D3DDevice = NULL;
// Matrices.
D3DXMATRIX g_projection;
// Vertex buffer to hold the geometry.
LPDIRECT3DVERTEXBUFFER9 g_VertexBuffer = NULL;
// Holds a texture image.
LPDIRECT3DTEXTURE9 g_Texture = NULL;
// Used for point sprites.
inline unsigned long FtoDW(float val) { return *((unsigned long*)&val); }
// Particle system.
CRainPS g_rainPS(0, -4, 0, 0, 1, 0);
LRESULT WINAPI MsgProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch(msg)
{
case WM_DESTROY:
PostQuitMessage(0);
return 0;
break;
case WM_KEYUP:
if(wParam == VK_ESCAPE) PostQuitMessage(0);
break;
}
return DefWindowProc(hWnd, msg, wParam, lParam);
}
int WINAPI WinMain(HINSTANCE hInst, HINSTANCE prevhInst, LPSTR cmdLine, int show)
{
// Register the window class
WNDCLASSEX wc = { sizeof(WNDCLASSEX), CS_CLASSDC, MsgProc, 0L, 0L,
GetModuleHandle(NULL), NULL, NULL, NULL, NULL,
WINDOW_CLASS, NULL };
RegisterClassEx(&wc);
// Create the application's window
HWND hWnd = CreateWindow(WINDOW_CLASS, WINDOW_NAME, WS_OVERLAPPEDWINDOW,
100, 100, WINDOW_WIDTH, WINDOW_HEIGHT,
GetDesktopWindow(), NULL, wc.hInstance, NULL);
// Initialize Direct3D
if(InitializeD3D(hWnd, false))
{
// Show the window
ShowWindow(hWnd, SW_SHOWDEFAULT);
UpdateWindow(hWnd);
// Enter the message loop
MSG msg;
ZeroMemory(&msg, sizeof(msg));
while(msg.message != WM_QUIT)
{
if(PeekMessage(&msg, NULL, 0U, 0U, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
else
RenderScene();
}
}
// Release any and all resources.
Shutdown();
// Unregister our window.
UnregisterClass(WINDOW_CLASS, wc.hInstance);
return 0;
}
bool InitializeD3D(HWND hWnd, bool fullscreen)
{
D3DDISPLAYMODE displayMode;
// Create the D3D object.
g_D3D = Direct3DCreate9(D3D_SDK_VERSION);
if(g_D3D == NULL) return false;
// Get the desktop display mode.
if(FAILED(g_D3D->GetAdapterDisplayMode(D3DADAPTER_DEFAULT, &displayMode)))
return false;
// Set up the structure used to create the D3DDevice
D3DPRESENT_PARAMETERS d3dpp;
ZeroMemory(&d3dpp, sizeof(d3dpp));
if(fullscreen)
{
d3dpp.Windowed = FALSE;
d3dpp.BackBufferWidth = WINDOW_WIDTH;
d3dpp.BackBufferHeight = WINDOW_HEIGHT;
}
else
d3dpp.Windowed = TRUE;
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
d3dpp.BackBufferFormat = displayMode.Format;
// Create the D3DDevice
if(FAILED(g_D3D->CreateDevice(D3DADAPTER_DEFAULT,
D3DDEVTYPE_HAL, hWnd, D3DCREATE_HARDWARE_VERTEXPROCESSING |
D3DCREATE_PUREDEVICE, &d3dpp, &g_D3DDevice))) return false;
// Initialize any objects we will be displaying.
if(!InitializeObjects()) return false;
return true;
}
bool InitializeObjects()
{
// Initialize particle system.
g_rainPS.Initialize(0, -3, 0, 3000, 2, 3, 2);
// 初始的时候这是一个很大的更新参数,创建3000个粒子
g_rainPS.Update(10);
// Create the vertex buffer. 创建3000个顶点 D3DFVF_XYZ | D3DUSAGE_POINTS
// 每个粒子其实就是一个顶点结构
// 因为将在RenderScene()函数中使用它。由于顶点缓存是动态的,因此可以动态设置,
// 而无需考虑与静态缓存相关的性能问题
if(FAILED(g_D3DDevice->CreateVertexBuffer(sizeof(CParticle) *
3000, D3DUSAGE_DYNAMIC | D3DUSAGE_WRITEONLY |
D3DUSAGE_POINTS, D3DFVF_XYZ, D3DPOOL_DEFAULT,
&g_VertexBuffer, NULL))) return false;
// Load the texture image from file.
if(D3DXCreateTextureFromFile(g_D3DDevice, "sprite.tga", &g_Texture) != D3D_OK) return false;
// Set the image states to get a good quality image.
g_D3DDevice->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);
g_D3DDevice->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);
// Set default rendering states.
g_D3DDevice->SetRenderState(D3DRS_LIGHTING, FALSE);
g_D3DDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE);
// Set the projection matrix.
D3DXMatrixPerspectiveFovLH(&g_projection, 45.0f, WINDOW_WIDTH/WINDOW_HEIGHT, 0.1f, 1000.0f);
g_D3DDevice->SetTransform(D3DTS_PROJECTION, &g_projection);
return true;
}
void RenderScene()
{
// Update the particle system.
g_rainPS.Update(0.2f);
// 因为创建顶点缓存用的是动态缓存D3DUSAGE_DYNAMIC,所以可以在每一帧渲染之前改变顶点缓存的数据
// Buffer pointer and size of particles we are copying.
void *p;
int num = sizeof(CParticle) * g_rainPS.m_particleCount;
// Copy particles in the vertex buffer.
if(FAILED(g_VertexBuffer->Lock(0, num, (void**)&p, 0))) return;
memcpy(p, g_rainPS.m_particles, num);
g_VertexBuffer->Unlock();
// Clear the backbuffer.
g_D3DDevice->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0,0,0), 1.0f, 0);
// Begin the scene. Start rendering.
g_D3DDevice->BeginScene();
// Draw square.
g_D3DDevice->SetTexture(0, g_Texture);
g_D3DDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
g_D3DDevice->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_ONE);
// 当启用sprite时,不用为顶点指定纹理坐标,系统会自动帮你处理
g_D3DDevice->SetRenderState(D3DRS_POINTSPRITEENABLE, TRUE);
g_D3DDevice->SetRenderState(D3DRS_POINTSCALEENABLE, TRUE);
g_D3DDevice->SetRenderState(D3DRS_POINTSIZE, FtoDW(0.02f));
g_D3DDevice->SetRenderState(D3DRS_POINTSIZE_MIN, FtoDW(0.02f));
g_D3DDevice->SetRenderState(D3DRS_POINTSCALE_A, FtoDW(0.0f));
g_D3DDevice->SetRenderState(D3DRS_POINTSCALE_B, FtoDW(0.0f));
g_D3DDevice->SetRenderState(D3DRS_POINTSCALE_C, FtoDW(1.0f));
g_D3DDevice->SetFVF(D3DFVF_XYZ);
g_D3DDevice->SetStreamSource(0, g_VertexBuffer, 0, sizeof(CParticle));
g_D3DDevice->DrawPrimitive(D3DPT_POINTLIST, 0, g_rainPS.m_particleCount);
g_D3DDevice->SetRenderState(D3DRS_POINTSPRITEENABLE, FALSE);
g_D3DDevice->SetRenderState(D3DRS_POINTSCALEENABLE, FALSE);
g_D3DDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, FALSE);
// End the scene. Stop rendering.
g_D3DDevice->EndScene();
// Display the scene.
g_D3DDevice->Present(NULL, NULL, NULL, NULL);
}
void Shutdown()
{
if(g_D3DDevice != NULL)
g_D3DDevice->Release();
g_D3DDevice = NULL;
if(g_D3D != NULL)
g_D3D->Release();
g_D3D = NULL;
//因为粒子系统使用了动态内存,所以使用完粒子系统后,必须确保释放动态内存
if(g_VertexBuffer != NULL)
g_VertexBuffer->Release();
g_VertexBuffer = NULL;
if(g_Texture != NULL)
g_Texture->Release();
g_Texture = NULL;
g_rainPS.Shutdown();
}
/*
因为粒子系统使用了动态内存,所以使用完粒子系统后,必须确保释放动态内存。
RenderScene()函数使用动态的Direct3D顶点缓存,所以可以使用3D场景中现有的当前粒子设置该顶点缓存。
当使用点状sprite时,使用系统释放的粒子当前总数,这样只有那些sprite可以被渲染,
而不是所有不存在的粒子都要被渲染。
*/