使用OpenGL实现粒子系统: 漂亮的喷雾

漂亮的喷雾

 

通过出现在像新闻组这样的公共论坛上的次数来判断, 粒子系统是一个相当热点的问题。 一部分原因可能是QUAKE使用了烟雾粒子, 血溅效果和火花落下效果, 并且获得了巨大成功。

 

但是可以肯定的是, 对粒子系统的兴趣和它们的能力有些关系, 相比其它计算机图形学方法, 更能实时实现自然现象。 William Reeves早在1982到1983年就实现了粒子系统。当工作于《星际迷航Ⅱ:可汗之怒》时, 他需要寻找一种方法来为创世纪演示序列创造逼真的火焰效果。 Reeves发现传统的擅于创建平滑清晰表面的对象的建模方式, 无法达到需求。 构成这种效果的对象不是由简单的确定的曲面组成的。 这些对象, 他称之为“模糊”, 将更好的被建模为一种粒子的系统, 这种系统通过一组动态规则运作。 粒子曾一直被用来实现像烟雾和星系这样的自然效果, 但是它很难控制。 Reeves意识到通过应用一个规定粒子的系统, 他可以在保留一些创造性的控制的同时达到一种混乱的效果。 于是粒子系统就出现了。

 

它是怎么工作的?

一个基本的粒子系统可以仅由一组空间中的3维顶点组成。 不像标准的几何对象, 粒子组成的系统不是静态的。 它们经历了一个完整的生命周期。 粒子产生, 随时间变化, 然后死亡。 通过调整影响生命周期参数, 你可以创建不同类型的效果。 另一个关于粒子系统的关键点是粒子是混乱的。 也就是说, 不是有一个确定的路径, 每个粒子可以有一个更改它们行为的元素, 成为一个随机过程(一个很好的书面用语), 这个随机过程使效果看起来非常真实自然。 这个月, 我要创建一个实时粒子系统, 展示一些基本技巧以及你可以创建的迷人效果。

 

粒子

我们先来看看一个粒子需要的属性。 首先, 我需要知道粒子的位置, 因为我还想能容易对粒子反锯齿, 所以我还要存储粒子的前一个位置。 我需要知道粒子当前移动的方向。 这可以存储为一个方向向量中。 我还需要知道粒子在这个方向的移动速度, 但是速度可以通过乘法简单的结合到方向向量中。 我要渲染粒子为彩色的点, 所以我还需要知道这个粒子的当前颜色和之前颜色以便于反锯齿。 为了让颜色随时间变化, 我把每帧的颜色变化量存储起来。 我需要的最后一个信息是粒子的生命计数, 它是这个粒子在死亡之前存在的帧数目。

表1 . 粒子结构.

struct tParticle

{

tParticle*prev,*next; // 双向链表, 指向前前驱结点和后继结点

tVector pos; // 当前位置

tVector prevPos;//之前位置

tVector dir; // 当前方向和速度

int life; // 还会存在的时间

tColor color; //粒子当前颜色

tColorprevColor; //粒子之前颜色

tColordeltaColor; //颜色改变量

};

  如表1, 我为粒子定义的数据结构。 如果你希望使你的粒子更复杂, 在这里增加属性会很容易。 你可以通过一个尺寸属性来变动粒子尺寸, 通过对颜色增加一个alpha值改变透明度。 此外, 你可以通过增加变量来增加质量属性或者其它物理属性。

表2 发射器结构

struct tEmitter

{

long id; //EMITTER ID

char name[80];// EMITTER NAME

long flags; //EMITTER FLAGS

//TRANSFORMATION INFO

tVector pos; //XYZ POSITION

float yaw, yawVar;// YAW AND VARIATION

float pitch,pitchVar; // PITCH AND VARIATION

floatspeed,speedVar;

// Particle

tParticle*particle; // NULL TERMINATED LINKED LIST

inttotalParticles; // TOTAL EMITTED AT ANY TIME

intparticleCount; // TOTAL EMITTED RIGHT NOW

intemitsPerFrame, emitVar; // EMITS PER FRAME AND VARIATION

int life,lifeVar; // LIFE COUNT AND VARIATION

tColorstartColor, startColorVar; // CURRENT COLOR OF PARTICLE

tColor endColor,endColorVar; // CURRENT COLOR OF PARTICLE

// Physics

tVector force; //GLOBAL GRAVITY, WIND, ETC.

};

发射器

  粒子发射器是在系统中负责创建粒子的实体。 这是你要在一个实时3D世界中放置来创建不同效果的对象。 发射器控制粒子的数量、 大体的发射方向以及其它全局设置。 发射器的数据结构如表2. 这也是我设置我下面要说的随机过程的地方。 例如, emitNumber是每帧应该发射的粒子数目的平均值。 emitVariance是从emitNumber加上或减去的随机数。 通过调整这两个值, 你可以通过改变常数来改变显示效果, 设置为稳定流或一个更加随机的流。 计算每帧发送的粒子数的公式:particleCount = emitNumber + (emitVariance* RandomNum()); 这里RandomNum()是一个函数, 返回-1.0到1.0之间的浮点数。

  这些技巧还用来更改颜色, 方向, 速度, 以及粒子的生命周期。 颜色是一个特例, 因为我想让颜色随粒子生命周期而变化。 我用上面的方法计算了两个随机的颜色值, 然后通过生命值划分它们的区别。 这创建了颜色delta, 它被加到每帧中的每个粒子中。

  现在我需要描述粒子应该发射的方向。 我们实际上只需要描述两个相对于初始的旋转角度, 因为粒子是空间中的单个点, 并且我不考虑旋转。旋转的两个角度是y轴的旋转(θ)和相对与x轴的(Ψ) 。 这些角度通过一个随机值改变, 然后转换为一个对每个粒子的方向向量。

  产生方向向量的转换过程很容易。 它需要一些基本的3D旋转技巧和一些基本矩阵变换知识。

  一个关于y的旋转被定义为

x’ =x*cos(q) + z*sin(q);

y’ =y;

z’ = -x*sin(q) + z*cos(q)

或者, 以矩阵的形式

一个关于x的旋转是

x’ =x;

y’ =y*cos(y) - z*sin(y);

z’ = y*sin(y) + z*cos(y)

一旦这两个矩阵被合并到一个单旋转矩阵中, 我得到了下面的矩阵:

  现在因为我在金酸方向向量, 我需要用这个矩阵乘向量(0, 0, 1)。 一旦所有的0被消去, 我得到最终代码片段, 如表3. 要完成粒子运动向量, 这个最终方向向量被一个速度标量乘, 当然, 这个速度值也是随机的。

  表3 转换旋转到一个方向向量

///

// Function:RotationToDirection

// Purpose:Convert a Yaw and Pitch to a direction vector

///

voidRotationToDirection(float pitch,float yaw,tVector *direction)

{

direction->x= -sin(yaw) * cos(pitch);

direction->y= sin(pitch);

direction->z= cos(pitch) * cos(yaw);

}

/// initParticleSystem

表4 添加一个新粒子到发射器

///

// Function:addParticle

// Purpose: adda particle to an emitter

// Arguments:The emitter to add to

///

BOOLaddParticle(tEmitter *emitter)

{

/// LocalVariables ///

tParticle*particle;

tColorstart,end;

floatyaw,pitch,speed;

///

// IF THERE ISAN EMITTER AND A PARTICLE IN THE POOL

// AND I HAVEN'TEMITTED MY MAX

if (emitter !=NULL && m_ParticlePool != NULL &&

emitter->particleCount< emitter->totalParticles)

{

particle =m_ParticlePool; // THE CURRENT PARTICLE

m_ParticlePool =m_ParticlePool->next; // FIX THE POOL POINTERS

if(emitter->particle != NULL)

emitter->particle->prev= particle; // SET BACK LINK

particle->next= emitter->particle; // SET ITS NEXT POINTER

particle->prev= NULL; // IT HAS NO BACK POINTER

emitter->particle= particle; // SET IT IN THE EMITTER

particle->pos.x= 0.0f; // RELATIVE TO EMITTER BASE

particle->pos.y= 0.0f;

particle->pos.z= 0.0f;

particle->prevPos.x= 0.0f; // USED FOR ANTI ALIAS

particle->prevPos.y= 0.0f;

particle->prevPos.z= 0.0f;

// CALCULATE THESTARTING DIRECTION VECTOR

yaw =emitter->yaw + (emitter->yawVar * RandomNum());

pitch =emitter->pitch + (emitter->pitchVar * RandomNum());

// CONVERT THEROTATIONS TO A VECTOR

RotationToDirection(pitch,yaw,&particle->dir);

// MULTIPLY INTHE SPEED FACTOR

speed =emitter->speed + (emitter->speedVar * RandomNum());

particle->dir.x*= speed;

particle->dir.y*= speed;

particle->dir.z*= speed;

// CALCULATE THECOLORS

start.r =emitter->startColor.r + (emitter->startColorVar.r * RandomNum());

start.g =emitter->startColor.g + (emitter->startColorVar.g * RandomNum());

start.b =emitter->startColor.b + (emitter->startColorVar.b * RandomNum());

end.r =emitter->endColor.r + (emitter->endColorVar.r * RandomNum());

end.g =emitter->endColor.g + (emitter->endColorVar.g * RandomNum());

end.b =emitter->endColor.b + (emitter->endColorVar.b * RandomNum());

particle->color.r= start.r;

particle->color.g= start.g;

particle->color.b= start.b;

// CALCULATE THELIFE SPAN

particle->life= emitter->life + (int)((float)emitter->lifeVar * RandomNum());

// CREATE THECOLOR DELTA

particle->deltaColor.r= (end.r - start.r) / particle->life;

particle->deltaColor.g= (end.g - start.g) / particle->life;

particle->deltaColor.b= (end.b - start.b) / particle->life;

emitter->particleCount++;// A NEW PARTICLE IS BORN

return TRUE;

}

return FALSE;

}

/// addParticle///

创建一个新粒子

  为了避免大量昂贵的内存分配开销, 所有的粒子被创建在一个通用粒子池中。 我选择把粒子池实现为一个链表。 当一个粒子被发射时, 他从通用粒子池中移除, 并且插入到发射器的粒子链表中。 尽管这限制了场景中的粒子总数, 却使速度增加了很多。 通过构造双向粒子链表, 当粒子死亡时很容易移除一个粒子。

  创建一个新粒子并把它添加到发射器中的代码如表4。 它操纵对全局粒子池所有的内存管理, 还对粒子设置全部的随机值。

  我选择简单的在发射器的源 点 创建每个新粒子。 在William Reeves的SIGGRAPH文章中, 他描述了产生粒子的不同方法。 连同点源,他还描述了在球面上创建粒子的方法,以及在球体内, 在2D圆盘的表面, 在矩形的表面。 这些不同的方法将创建不同的效果, 所以你应该通过实验来找到最适合你的应用的方法。

表5 更新粒子

///

// Function:updateParticle

// Purpose:updateParticle settings

// Arguments:The particle to update and the emitter it came from

///

BOOLupdateParticle(tParticle *particle,tEmitter *emitter)

{

// IF THIS IS AVALID PARTICLE

if (particle !=NULL && particle->life > 0)

{

// SAVE ITS OLDPOS FOR ANTI ALIASING

particle->prevPos.x= particle->pos.x;

particle->prevPos.y= particle->pos.y;

particle->prevPos.z= particle->pos.z;

// CALCULATE THENEW

particle->pos.x+= particle->dir.x;

particle->pos.y+= particle->dir.y;

particle->pos.z+= particle->dir.z;

// APPLY GLOBALFORCE TO DIRECTION

particle->dir.x+= emitter->force.x;

particle->dir.y+= emitter->force.y;

particle->dir.z+= emitter->force.z;

// SAVE THE OLDCOLOR

particle->prevColor.r= particle->color.r;

particle->prevColor.g= particle->color.g;

particle->prevColor.b= particle->color.b;

// GET THE NEWCOLOR

particle->color.r+= particle->deltaColor.r;

particle->color.g+= particle->deltaColor.g;

particle->color.b+= particle->deltaColor.b;

particle->lifeÑ;//IT IS A CYCLE OLDER

return TRUE;

}

else if(particle != NULL && particle->life == 0)

{

// FREE THISSUCKER UP BACK TO THE MAIN POOL

if(particle->prev != NULL)

particle->prev->next= particle->next;

else

emitter->particle= particle->next;

// FIX UP THENEXTÕS PREV POINTER IF THERE IS A NEXT

if(particle->next != NULL)

particle->next->prev= particle->prev;

particle->next= m_ParticlePool;

m_ParticlePool =particle; // NEW POOL POINTER

emitter->particleCountÑ;// ADD ONE TO POOL

}

return FALSE;

}

/// updateParticle///

更新粒子

  一旦粒子产生了, 它就由粒子系统处理。 更新过程的代码如表5. 对每个模拟的每个生命周期, 粒子会被更新。 首先, 检查某粒子是否死亡, 如果它已经死亡, 粒子从发射器链表中移除并返回到全局粒子池中。 这时, 全局设置被应用到方向向量中, 并且颜色也被更改。

 

渲染粒子系统

  简单的看, 粒子系统就是一组点的集合, 因此它可以像一组3D彩色点来渲染。你还可以计算一个围绕点的多边形, 来使得它总是像一个广告版面对整个镜头。 然后应用任何你喜欢的纹理到这个多边形。 通过缩放多边形到镜头的距离, 你可以创建透视效果。 另一个选择是在粒子的位置绘制任何类型的3D对象。

  我采用简单的路线。 我仅仅把每个粒子绘成一个3D顶点。 如果你开启发锯齿, 系统从前一个位置和颜色到新的位置和颜色绘制一个高落德着色线。 这通过耗费一个渲染速度来使看起来平滑。 你可以在图1a和1b中看到其中的不同。 第一个图像是简单的点渲染, 第二个图像由线段组成。

 

你可以用它干什么?

  一旦你设计好了你的系统, 你可以开始构建效果。 你可以简单的构建一些效果像火焰, 喷泉, 烟花, 只需要通过简单的修改发射器的属性。 通过附加发射器到另一个对象并激活它, 你可以创建简单的烟雾拖尾或彗星的尾巴。

  你还可以通过在粒子死亡时创建新的粒子系统创建更加复杂的效果。 在《星际迷航Ⅱ》中的创世纪序列实际上拥有最多400个粒子系统包含750,000个粒子。 对于你的实时血液喷射效果来说这可能有点多, 但是随着硬件变得越来越快, 谁又知道呢?

  另外, 我的简单的物理模型可以被大幅修改。 大部分粒子可以是随机的, 引起引力使它们出现不同的效果。 一个摩擦模型会迫使一些粒子在运动时减速。 另外的近地空间效果, 像磁场, 风吹, 旋转漩涡, 会更大程度的改变粒子。或者你可以在生命周期中改变emitsPerFrame来创建烟雾膨胀的效果。

  我在一些商业粒子系统的实现中见到很多想法。 你可以随时间变动粒子的尺寸来创建一个分散效果。 在生命周期中增加更多的关键位置来创建更复杂的样子。另外一个有趣变种是用粒子系统创建植物。 通过在生命周期中跟踪每个位置, 然后渲染出一条贯穿这些点的线, 你可以得到一丛草的对象。 像这样的有机对象是难以用多边形建模的。 另外一个扩展领域是碰撞检测。 你可以创建弹射像立方体或者球体的边界对象来简单的反射一个方向向量。

  你可以从我上面描述这些想法中来探索用粒子系统可以创建什么。 通过创建一个灵活的粒子引擎, 你可以通过修改一些设置完成很多不同效果。 这些灵活的发射器可以很容易的放进已有的3D实时引擎中来增加一个仿真的真实性和趣味性。

  源代码和应用程序展示了粒子系统的使用。 发射器设置可以通过一个对话框定制效果。 这些设置可以被保存起来, 作为一个发射器库(包含很多发射器, 每种发射器是一种效果)。 要获取源代码和程序, 请到Game Developer的站点www.gdmag.com。

 

参考

Reeves, William T.Particle Systems. A Technique for Modeling a Class of Fuzzy Objects.ComputerGraphics, Vol. 17, No. 3 (1983): 359-376.

Reeves, William T.Approximate and Probabilistic Algorithms for Shading and Rendering StructuredParticles Systems.Computer Graphics, Vol. 19,

No. 3 (1985):313-322.

Watt, Alan, 3DComputer Graphics.

Reading, Mass.:Addison Wesley, 1993.

本文的英文原版很源代码实现请见:http://download.csdn.net/detail/xingtianxia710/4004962

+---------------------------------------------------------+

|                   翻译by Super Zhao                         |

+---------------------------------------------------------+

你可能感兴趣的:(Computer,Graphics)