请大家想象游戏中的这样一个场景:一枚火箭拖着尾烟划过天际,突然火箭爆炸了,碎片四处飞溅。其中,一具生物的尸体向你飞来,它的碎片飞散开来,带起蓬蓬血雾,然后在镜头上留下混乱的血肉痕迹。这个场景中的所有这些元素的共同点是什么呢?
是的,多数元素都是混乱的。但从技术的角度讲,本场景中的大多数效果得益于一个优秀的粒子系统。烟,火花,血这些现代游戏中的效果通常使用粒子系统来创建。
为了实现这些效果,你需要构件一个粒子系统,而且不仅仅是一个简单的系统。你所需要的是一个高级粒子系统,一个快速,便捷,可扩展的系统。如果你是初次接触粒子系统,我推荐你线阅读Jeff Lander的文章(The Ocean Spray in Your Face, Graphic Content, July 1998)。Lander的文章和本文的区别在于,前者描述了粒子系统的基本概念,而这里我着重于如何构建更高级的系统。我将会随本文提供完整的源代码,你可以下载以验证本系统。
性能和需要
高级粒子系统可能会需要大量的代码,所以设计好数据结构是非常重要的。此外必须牢记如果设计欠佳,粒子系统会大幅降低刷新率,并且大多数的性能问题是由粒子系统带来的内存管理问题引起的。
设计粒子系统时首先应该明白粒子系统大大增加每帧的可见多边形数量。每个粒子可能需要四个顶点和两个三角形。以此计算,一个场景中的2,000个可见的雪花粒子将增加4,000个可见的三角形。又因为大多数粒子是运动的,我们不能预先计算顶点缓冲,所以每一帧,定点缓冲都需要被改变。
技巧在于只执行尽量少的内存操作(分配和释放)。这样,如果一个粒子在一定时间后消亡,不要急着从内存中释放。相反,用一个标志来记录它是死亡还是重生(重新初始化)。然后当所有的粒子都被标为死亡时,释放整个粒子系统。(包括系统中的所有粒子),如果系统是定常的,那么使系统保持存活。如果你想要重建系统或只想假入一个新的粒子,你应该根据粒子所属的系统使用相应的默认设置/属性来自动初始化粒子。
例如,我们在构建一个“烟”系统。当你创建或重建一个粒子时,你可能按表一中的值来设定(当然,血和烟的初始颜色,能量,大小,速度是不同的)。注意各个值亦依赖于系统本身的设定。如果你为烟系统设定风以使烟飘向左方,那么新粒子的速度必然不同于不受风影响的垂直升起的烟系统。如果你有一个定常的烟系统,且烟粒子的能量是零(如此你就看不见它了),你会想重新初始化它的设定以使它满载能量地又从底部升起。有的粒子系统可能需要粒子以不同的方式被渲染。例如,你或许会想要几种血系统,例如喷血,溅血,血池和溅在镜头上的血痕,等等。每个效果包含特定的粒子。“喷血”将渲染血在空中喷射的景象,当这些血喷在墙壁上,会调用“溅血”系统,营造出血迹在墙和地板溅开的效果。血池系统创建出某人被击毙后地板上的几滩血迹。
数据类型 |
名称 |
描述 |
Vector3 |
position |
空间中的粒子位置 |
Vector3 |
oldPos |
粒子的先前位置,有时有用 |
Vector3 |
velocity |
粒子速度(position += velocity) |
dword |
color |
粒子颜色(顶点颜色) |
int |
energy |
粒子的能量 |
float |
size |
粒子的大小 |
|
表一 |
每个粒子系统都以一种独特的方式工作。溅血和烟雾的渲染方式是有很大区别的。烟雾粒子通常面对活动镜头,而溅血则映射到血溅到的平面上。
当构建粒子系统时,必须考虑到游戏中所有可能的可变参数,并把他们灵活地建入系统。让我们再次以烟雾系统为例。我们可能会想改变风的方向向量以使当汽车驶过烟雾时,烟粒子受到汽车带起的风的影响。
从这点你会意识到每一个系统对于自己的作用而言都是很特殊的。但如果我们想要以某种方式控制系统中还不被系统规则支持的粒子时,该怎么办?为了达到这个目的,我们还需要创建一个“手工”粒子系统,以使我们能够每帧刷新所有的粒子属性。
最后我们要考虑的是在引擎层次中连接粒子系统的能力。或许我们想要为一支香烟连接一个烟雾系统或发光系统。如果角色移动,连接到香烟的粒子系统应该得到正确的刷新。
这就是高级粒子系统的一些基本要求。在下一节,我将讲述如何设计一个性能优良的数据结构以实现上述的所有特性。
创建数据结构
现在我们知晓我们需要什么样的特性。图一显示了将要创建的系统的概貌。请注意在图中有一个粒子管理器(particle manager),稍候我将对此做出解释。
在此我们采用从下到上的步骤来设计类,首先是粒子类(particle class)。
粒子类(particle class)。如果你曾经建过粒子系统,你会知道粒子必需的属性。表一列出了其中一些常见的属性。
注意粒子的先前位置也可能有用。例如,你可能会想在一个粒子的先前和当前位置之间画它。火花就是一个从中受益的好例子。你可以在图2中看到我创造的火花效果。
色彩和能量属性也能够用来创建一些有趣的特效。在前一个粒子系统中,我在烟系统中使用色彩,这样我能够在场景中动态地为粒子进行光照渲染。
能量值也是非常重要的。能量是与粒子的生命周期相关的-你可以利用这一点来判断一个粒子是否消亡。而且由于某些粒子的色彩和强度随时间变化(例如火花),你或许会想把它与定点色彩的alpha通道值联系起来。
我强烈推荐把粒子类的构造函数设为空,因为你并不想在构造的时候使用默认值,这是由于对大多数的例子系统来说,这些值总是不同的。
粒子系统类(the particle system class)这个类是整个系统的核心。刷新粒子的属性和设置粒子的形状都在这个类中进行。我现有的粒子系统类使用我的3D引擎的节点类,其中包含例如位置向量、旋转四元数和比例值的数据。由于我继承了这个节点类的所有成员,我能够在引擎的层次结构中连接我的粒子系统,使得引擎能够改变粒子系统的位置,就如同上面讨论过的香烟系统一样。但如果你的引擎没有层次支持,或者如果你在构建一个独立的粒子系统,这就不需要了。表2列出了在粒子系统基类中你所需要的属性。
下面将讲述如何计算经常面对活动镜头的粒子的四个位置。首先,把粒子世界空间坐标变换到镜头空间(用世界空间的位置坐标乘以活动镜头矩阵)使用粒子的大小属性来计算四个顶点。
我们使用这四个形成形状的顶点来渲染粒子,虽然一个粒子只有一个位置:xyz。为了渲染一个粒子(例如一个火花),我们需要建立一个形状(用4个顶点)。然后在四个顶点之间我们得到两个三角形。想象一个非伸展的粒子总是面对你前面的镜头,就像图3所描述的一样。对我们来说,粒子总是面对活动镜头的,这意味着我们可以简单的加减粒子的镜头空间坐标的x和y值。换句话说,保留z值不变就好像你在作2D一样。你可以在列表1中看到一个计算的例子。
函数(the functions)
现在我们知道在粒子系统的基类中需要什么属性,我们能够开始思考需要什么样的函数.既然是基类大多数的函数被声明为虚函数.每种粒子系统都用不同的方式来更新粒子属性,所以我们需要一个虚拟的更新函数.这个更新函数将完成以下任务: 更新所有粒子的位置和其他属性; 如果我们不能预先计算绑定盒,则需更新它; 计算存活的粒子数量.如果没有存活的粒子则返回false,反之返回true.返回值将被用于判定系统是否可被删除. 现在我们的基类已有能力来更新粒子,我们也将要建立可用新的(也可能是旧的)位置来构建的形状.这个函数,SetupShape,必须是个虚拟函数,因为某些粒子系统会要伸展粒子,而有些系统不会这么做.你可以在清单1中看到这样一个函数.
要向给定的系统加入一个粒子或是重新生成它,建立一个做这件事的函数将是十分有益的.同样,这应该是个虚拟函数,并应该如此定义:virtual void SetParticleDefault(Particle &p);
就像我在前面解释过的,这个函数初始化给定粒子的属性值.但如果你想要改变烟的速度或是影响你的烟系统的风向.这是我们接触下一个主题:粒子系统的构造函数.许多粒子系统需要他们独特的构造函数,强迫我们在基类中建立一个虚拟构造函数和析构函数.在基类的构造函数中你应该输入以下信息:
你一开始想在系统中构建的粒子数量;
粒子系统的位置;
你想在系统中使用的混合模式;
你想在系统中使用的底纹或底纹文件名;
系统方式(即ID).
在我的引擎中,粒子系统的基类中的构造函数是这样的:
virtual ParticleSystem(int nr, rcVector3 centerPos, BlendMode blend=Blend_AddAlpha, rcString file name=óEffects/Particles/ green_particleó, ParticleSystemType type=PS_Manual);
那么应该在哪儿做例如烟系统的风向的各种设置呢?你可以在构造函数中加入根据特定系统的设定值,也可以在每个类中建立一个名为InitInfo的结构(structure),其中包含所有的必须的设定值.如果你使用后一种方法,确保在构造函数中加入一个新的参数,即指向新结构的指针.如果指针为空(NULL),使用默认的设定值.
正如你所想象的,第一种方法要在构造函数中加入很多参数,而这对于程序员来说过于繁琐.这是我不使用第一种方法的主要原因.使用第二种方法则简单得多,我们可以在每一个粒子系统类中建立一个函数以用默认值来初始化它的结构.这段代码的例子和演示程序可在Game Developer网站(http://www.gdmag.com)或我的网站(http://www.mysticgd.com)上找到.
粒子管理器(the Particle Manager)
现在我们已经发现了每个粒子系统背后的隐藏的技术,该是创建一个管理类来控制我们的各种粒子系统时候 .一个管理类将掌管创建,释放,更新和渲染所有的系统.这样,管理类必须有一个属性是指向粒子系统的指针.我强烈推荐你建立或是使用一个数组模版(array template),因为这将会简单些.
你制作的粒子系统的使用者或许会希望较容易地增加系统.他们也不想跟踪所有的系统以保证所有粒子均已死亡从而可以从内存中释放它们.这就是设计管理类的目.管理器将会在需要时自动更新和渲染系统,并删除已死亡的系统.
当使用不定时系统(系统将在给定的时间后死亡)时,有一个检查系统是否已被删除的函数是很有用的(例如,是否它还存在于粒子管理器中).想象你创建了一个系统并存储了指向系统的指针.你通过这个指针每一帧访问系统.如果系统恰巧在你使用指针之前死亡,会发生什么?崩溃.这就是为什么我们需要一个检查系统是否存活还是已经被管理类删除的函数.粒子管理类中需要的函数列表如表3所示.
AddSystem 函数可能只有一个参数:指向粒子类的指针.这将允许你根据需要轻易地增加一个烟系统或火系统.下面是我如何在引擎中增加一个粒子系统的例子: gParticleMgr->AddSystem( new Smoke(nrSmokeParticles, position, ...) ); 在世界更新函数中我调用了gParticleMgr->Update()函数,这个函数自动更新所有的系统并释放死亡的系统.Render函数然后渲染所有的可见粒子系统.
因为我们不想在每一帧跟踪系统中的所有粒子以判断是否所有的粒子均已死亡(这样系统可被删除),我们将使用Update函数来代替.如果这个函数返回TRUE,这意味着系统处于存活状态;反之系统已死亡并可以被删除.粒子管理程序的Update函数如清单2所示.
在我的粒子系统中,其中分配的所有具有相同底纹和混合方式的粒子将连续地渲染,同时将底纹转换和上载减到最小.这样,如果在屏幕上有十个可见的烟系统,就只有一个底纹转换和状态改变将被执行.
设计,然后再编码
设计一个灵活的,快速的和可扩展的高级粒子系统并不很困难,如果你花时间来考虑你将如何在游戏中使用它,并且由此细致设计你的系统.由于在此讨论的系统使用带有继承的类,你也可以在.DLL文件中加入独立的粒子系统形式.这就使建立某种plug-in系统成为可能,而这可能是某些开发者所感兴趣的. 你也可以下载我的粒子系统(我为Oxygen3D开发的最新的引擎)的源程序.这些源码不是独立的可编辑系统,但如果你遇到问题,它应该能帮助你.如果你还有任何问题或意见,请直接给我发电子邮件.
以下是列表或清单
/********************************************************************************* LISTING1.Calculating the shape of a particle facing the camera. void ParticleSystem::SetupShape(int nr) { assert(nr < shapes.Length()); // make sure we don't try to shape anything we // don't have // calculate cameraspace position Vector3 csPos = gCamera->GetViewMatrix() * particles[nr].position; // set up shape vertex positions shapes[nr].vertex[0] = csPos + Vector3(-particles[nr].size, particles[nr].size, 0); shapes[nr].vertex[1] = csPos + Vector3( particles[nr].size, particles[nr].size, 0); shapes[nr].vertex[2] = csPos + Vector3( particles[nr].size, -particles[nr].size, 0); shapes[nr].vertex[3] = csPos + Vector3(-particles[nr].size, -particles[nr].size, 0); } *********************************************************************************/
TA B L E 2 . Particle system base class attributes. |
Data type |
Name |
Description |
Texture |
*texture |
A pointer to a texture, which all particles will use. For performance reasons, we only use one texture for each individual particle system; all particles within the specific system will have the same texture assigned. |
BlendMode |
blendMode |
The blend mode you want to use for the particles. Smoke will probably have a different blend mode from blood-that's the reason you also store the blend mode for each particle system. |
int |
systemType |
A unique ID, which represents the type of system (smoke or sparks, for example). The systemType identifier is also required, since you may want to check for a specific type of particle system within the collection of all systems. For example, to remove all smoke systems, you need to know whether a given system is a smoke system or not. |
Array Particle |
particles |
The collection of particles within this system. This may also be a linked list instead of an array. |
Array PShape |
shapes |
A collection of shapes, describing the shapes of the particles. The shape descriptions of the particles usually consist of four positions in 3D camera-space. These four positions are used to draw the two triangles for our particle. As you can see in Table 1, a particle is only stored as a single position, but it requires four positions (vertices) to draw the texture-mapped shape of the particle. |
int |
nrAlive |
Number of particles in the system which are still alive. If this value is zero, it means all particles are dead and the system can be removed. |
BoundingBox3 |
boundingBox |
The 3D axis-aligned bounding box (AABB), used for visibility determination. We can use this for frustum, portal, and anti-portal checks. |
TA B L E 3. Particle manager class functions. |
Init |
Initializes the particle manager. |
AddSystem |
Adds a specified particle system to the manager. |
RemoveSystem |
Removes a specified particle system. |
Update |
Updates all active particle systems and removes all systems which died after the update. |
Render |
Renders all active and visible systems. |
Shutdown |
Shuts down the manager (removes all allocated systems). |
DoesExist |
Checks whether a given particle system still exists in the particle manager (if it has not been removed yet). |
/********************************************************************************* LISTING2. Update function of the particle manager. for (int i=0; i { (编辑者:此句有误) if (!particleSystems[i]->Update()) // if the system died, remove it { delete particleSystems[i]; // release it from memory particleSystems.SwapRemove(i); // remove number i, and fill the gap // with the last entry in the array } else i++; } *********************************************************************************/ |