粒子系统(Particle System

最后更新日期:201626

1.      粒子系统简介

什么是粒子系统?[1][2]中给出的定义是:

A particle system is a set of separate small objects that are set into motion using some algorithm.

即粒子系统是一定数量微小物体的集合,每个物体的运动行为遵守给定的算法。粒子系统的特点是小物体数量众多;处于运动之中;有生命期(出生和死亡);使用透明渲染,否则会相互遮挡。在计算机图形可视化应用中,粒子系统通常用来模拟火焰、烟雾、爆炸、水流、风等现象。

[3]中给出了一个C++示例程序。程序虽短,但却给出了一个粒子系统的基本属性和方法。[4]同样讨论了粒子系统开发中的数据结构设计和性能问题。[5]介绍了设计一个粒子系统所应考虑的问题和基本数据结构。[8]给出了一个使用DirectX 11Win32桌面示例程序,里面没有使用流输出和sprite技术,但使用了动态顶点缓冲区以实现粒子的更新。[9][给出了基于OpenGL ESiOS系统下的粒子系统示例程序。

总结起来,实现粒子系统的常用方法有三种:

1.         创建一个包含所有粒子数据的顶点数组(及相应的vertex buffer)。优点:实现简单,可移植性好。缺点:效率不高(参考[6]中第17.6节开始处的讨论)。

2.         使用geometry shader。优点:效率高。缺点:GPU必须支持GS,可能无法在旧计算机上运行。

3.         使用Instancing技术,优缺点介于12两种方法之间。

 

2.      Instancing技术

在讨论粒子系统之前,有必要先介绍一下Instancing技术。

Instancing是指在场景中多次绘制同一物体,但是在每次绘制中物体的属性(位置,大小,方位,纹理,颜色等)会发生改变[6]。例如在绘制森林时要画很多棵树。如果我们把每棵树作为一个独立的物体进行渲染,那么API调用开销会非常大。在Instancing中,我们只需维护一份物体的几何信息(顶点数据和索引数据)。然而,我们还是要维护物体在每次绘制时的属性信息(如变换矩阵,颜色等),我们称之为实例数据(instanced data)。

Direct3D中,向GPU发送实例数据的方法与发送顶点数据的方式相同,都是使用IASetVertexBuffers函数。但区别在于在D3D11_INPUT_ELEMENT_DESC结构中的InputSlot InputSlotClass要设置为1D3D11_INPUT_PER_INSTANCE_DATA,而顶点数据分别为0D3D11_INPUT_PER_VERTEX_DATA。创建实例缓冲区(Instanced buffer)时,应该设为动态,即D3D11_BUFFER_DESC中的UsageBUFFER_USAGE_DYNAMIC。最后渲染时调用函数DrawIndexedInstanced或者DrawInstanced

游戏FallFury [7]给出了使用Direct3D实现Instancing的例子。该程序中的粒子系统是在Windows 8 示例程序“Direct3D sprite sample”的基础上开发的。Sprite的实现方法就使用了Instancing技术。

注意到使用Direct3D实现Instancing要求GPU至少支持DirectX 9.3

 

OpenGL中,我们通常用一个VBO(例如m_billboardBufferObj)表示物体顶点数据,用另外一个VBO(例如m_instanceBufferObj)表示实例数据。在创建m_instanceBufferObj 的时候,给其赋值时可以设置数据地址指针为空,即glBufferData的第三个参数为NULL。在更新实例数据的时候再指定数据[11]。在渲染时,需要使用glVertexAttribDivisorglDraw*Instanced这两个函数。[10] 给出了OpenGL API实现Instancing的例子。注意到使用OpenGL实现Instancing要求GPU至少支持OpenGL 3.3

3.      基于Direct3D 10 stream output stage实现的粒子系统

[2][3]中的粒子系统是用CPU实现的,然而为了获取最好的性能,现代粒子系统更多使用GPU实现。除了使用Instancing技术外,还有一些现代GPU提供的高级技术。它们能够提供比Instancing更高的性能,但也需要最新的硬件支持。

 

[6]中第20章给出了使用Direct3D 10的粒子系统实现。其核心方法是使用Direct3D 10流水线中的流输出阶段(stream output stage),将geometry shaderGS)的输出写到绑定到SOvertex buffer中。这样,粒子的整个更新过程在GPU(即GS中)完成。当实现Effect时,要实现两个technique

第一个technique中的GS实现流输出(包括粒子的生成和消灭),pixel shaderPS)设置为空,禁用深度/模版检测。目的是不显示GS输出的顶点。

第二个technique中的GS实现粒子的渲染,通常使用billboard技术把一个顶点转换成四边形(火焰)或者线段(雨滴)。有时为了显示所有粒子(不让某些粒子被它前面的粒子遮挡),需要把D3D11_DEPTH_STENCIL_DESC类型变量的DepthWriteMask设置为D3D11_DEPTH_WRITE_MASK_ZERO

4.      基于OpenGL Transform Feedback设计的粒子系统

TODO

5.      总结

5.1.     粒子系统设计原则

1,存放粒子数据的数组应该预先分配得足够大。在创建数组时执行一次内存分配操作,之后不应再执行内存分配操作,直到数组生命期结束后释放内存。避免频繁的动态内存分配,如new/mallocstd::vectorpush_back,resize等。

2,一般使用StructureOfArrays,而不是ArrayOfStructures,以避免浪费内存。

3,如果在渲染粒子时打开Blending,需要将所有粒子按从后到前的顺序排序。应该使用性能最好的排序算法,如heap sortquick sort等。

4,粒子系统的基类应该至少声明以下三个方法:

Generate:生成粒子系统,分配数组内存,给出初始值。

Update:更新粒子系统,包括系统中粒子的出生和死亡,属性的改变等。

Render:绘制粒子系统。

这三个函数应该声明为虚拟函数(virtual functions),以便子类继承和重定义(override)。

5.2.    粒子系统实现细节

1,当粒子系统开始工作后,应用程序主框架应该不断调用粒子系统渲染函数(很多应用程序仅在必要时进行渲染以减少功耗),并至少提供应用程序总运行时间和连续两次调用渲染函数之间的时间间隔。

一种方法是使用定时器,设计一个定时器变量particleSystemTimer和两个计时变量:particlesTimeDeltaparticlesTimeTotal。前者是定时器时间间隔,后者是从定时器最近一次启动后的累计时间。一个棘手的问题是在什么时候(代码的哪个位置)启动和停止定时器。有很多事件可以导致系统需要启动/停止定时器:

l  一个使用粒子系统的图层被创建(打开对话框、文件拖曳、命令行参数);

l  修改某一图层属性使其使用粒子系统;

l  一个使用粒子系统的图层被删除。

我们应该在这些事件中做启动/停止定时器这件事。

 

2,最好提供粒子系统渲染频率(FPS),并作为选项提供给用户。

 

3,对于复杂的应用,最好设计一个类(如ParticleSystemManager)来管理所有粒子系统。该类应该至少提供以下方法:

 

4,每一次粒子系统被主框架调用时至少包括更新和渲染两个过程。

6.      自制粒子系统演示程序ParticleSystemDemo

6.1.    基于DirectX 11Windows商店应用示例

想运行示例程序的读者需要在自己的系统上安装Visual Studio 2012或以上版本。Visual Studio家族中Express版本是免费的。可以直接使用Visual Studio 2012打开ParticleSystemDemo.sln文件,也可以使用Visual Studio 2012新建一个项目,步骤如下:

1,创建一个Visual C++, Windows Store, Direct2D App (XAML)项目(Visual Studio 2013中是DirectX App (XAML))。

2,在项目中添加如下文件:

BasicCamera.cpp, BasicCamera.h

BasicLoader.cpp, BasicLoader.h

BasicReaderWriter.cpp, BasicReaderWriter.h

BasicShapes.h

BasicSprites.GeometryShader.gs.hlsl

BasicSprites.GeometryShader.vs.hlsl

BasicSprites.Instancing.vs.hlsl

BasicSprites.ps.hlsl

DDSTextureLoader.cpp, DDSTextureLoader.h

mRenderMath .cpp, mRenderMath.h

particle.dds

ParticleRendererD3D.cpp, ParticleRendererD3D.h

ParticleRendererD3DSprite.cpp, ParticleRendererD3DSprite.h

ParticleRendererD3DStreamline.cpp, ParticleRendererD3DStreamline.h

ParticleSystem.cpp, ParticleSystem.h

ParticleSystemSprites.cpp, ParticleSystemSprites.h

ParticleSystemWindStreamline.cpp, ParticleSystemWindStreamline.h

SimpleParticleRenderer.cpp, SimpleParticleRenderer.h

Streamline.GeometryShader.gs.hlsl

Streamline.GeometryShader.vs.hlsl

Streamline.Instancing.vs.hlsl

Streamline.ps.hlsl

windstreamdata.txt

方法是在Solution Explorer中右项目名称,选择“Add, Existing Item”。

3,去掉文件SimpleTextRenderer.cppSimpleTextRenderer.h。然后将项目代码所有SimpleTextRenderer出现的地方替换为SimpleParticleRenderer

方法是在Solution Explorer中右将要被删除的文件,然后选择“Exclude from Project”。

4,禁用Precompiled Header

5,为所有hlsl文件设置Shade Type

6,增加预编译宏定义_CRT_SECURE_NO_WARNINGS

 

启动程序后,Windows商店应用窗口默认占据整个屏幕。用户在应用窗口中单击鼠标右键调出App Bar,如图1所示。点击App Bar中的“Previous Demo”和“Next Demo”切换粒子系统。

1:用户在应用窗口中单击鼠标右键调出App Bar,点击App Bar中的“Previous Demo”和“Next Demo”切换粒子系统。

 

6.2.    基于OpenGL 3GLUT桌面应用示例

这个示例程序是基于OpenGL API开发的,需要用到GLEW[13]GLUT[14]函数库。目前程序在以下系统中通过了编译,并能够正常运行:

l  Windows 7, Visual Studio 2008

l  Windows 8.1, Visual Studio 2010

l  Mac OS X 10.9, XCode 6.2

l  CentOS Linux 5.3, GCC 4.1.2

 

编译程序之前首先使用CMake[15]生成相应平台下的项目文件。关于CMake的使用方法请参考相关文档。运行程序之前需要把以下资源文件和GLSL文件放到可执行程序目录下面:

particle.dds, particle_wedge.dds, particle_ellipse.dds, windstreamdata.txt,

ParticleSpary.fsh, ParticleSpary.vsh

ParticleSprite.fsh, ParticleSprite.vsh

ParticleStreamline.fsh, ParticleStreamline.vsh

 

启动程序后,用户可以按键盘上的“+”和“-”切换粒子系统。

6.3.    基于OpenGL ESiOS应用示例

按照以下步骤使用XCode 6创建一个iOS Game Application项目。

1,新建iOS, Application, Game项目。在Choose options for your new project对话框中,输入如下信息:

Product Name: ParticleDemoiOS

Organization Name:

Organization Identifier:

Language: Objective-C

Game Technology: OpenGL ES

Device: Universal

创建完项目后转到Project Settings

Deployment Target: 7.0

 

2,把所有.m文件更名为.mm,以使C++Objective-C文件共存。

 

3,加入OpenGLES.frameworkGLKit.frameworkUIKit.framework框架。这时可运行程序看看项目配置的正确性。应该显示两个旋转的立方体。

 

4,修改文件GameViewController.mm,去掉与两个立方体显示相关的代码。

 

5,把源代码和资源文件加入项目中。对资源文件最好建一个组(右击ParticleDemoiOS,选择New Group)。把下列四个文件加入Resources组:particle.dds, particle_wedge.dds, particle_ellipse.dds, windstreamdata.txt。把六个GLSL文件(ParticleSpary.fsh, ParticleSpary.vsh, ParticleSprite.fsh, ParticleSprite.vsh, ParticleStreamline.fsh, ParticleStreamline.vsh)放入Shaders组。其他文件直接加入到ParticleDemoiOS下面。

 

6,修改Build Settings,在Preprocessor Macros中增加宏定义:OPENGL_USE_COREPROFILE

 

编译并运行程序,用户通过在视图中单击鼠标或者轻点(tap)来切换粒子系统。

6.4.    类的设计与组织

两个示例程序均使用C++开发。粒子数据使用类ParticleData表示。下面是类的定义:

#define PARTICLE_ATTRIBUTE_POSITION         0x00000001

#define PARTICLE_ATTRIBUTE_VELOCITY         0x00000002

#define PARTICLE_ATTRIBUTE_ACCELARATION     0x00000004

#define PARTICLE_ATTRIBUTE_COLOR            0x00000008

#define PARTICLE_ATTRIBUTE_AGE              0x00000010

#define PARTICLE_ATTRIBUTE_SIZE             0x00000020

#define PARTICLE_ATTRIBUTE_MASK             0x0000003F

class ParticleData

{

public:

    ParticleData(size_t capacity=0, int attributes=PARTICLE_ATTRIBUTE_POSITION) { initialize(capacity, attributes); }

    ~ParticleData();

 

    void Reset(size_t capacity, int attributes) { initialize(capacity, attributes); }

 

    // Use structure of arrays (SoA) rather than array of structures (AoS)

    // in order to make it easy to add/remove field to the particle

    // Allocate one huge buffer that contains maximum number of particles,

    // avoid dynamic memory allcation such as std::vector<Particle>

#if defined(WIN32) && _MSC_VER<=1500

    // Visual C++ 2008 does not support std::unique_ptr

    float3* position;

    float3* velocity;

    float3* acceleration;

    unsigned int* color;

    int* age;

    float *size;

#else

    std::unique_ptr<float3[]> position;

    std::unique_ptr<float3[]> velocity;

    std::unique_ptr<float3[]> acceleration;

    std::unique_ptr<unsigned int[]> color;

    std::unique_ptr<int[]> age;

    std::unique_ptr<float[]> size;

#endif

 

    // We keep the alive (dead resp.) particles to the front (end reps.) of

    // the buffer. The benefit is that when we render particles, there is

    // no need to check if it is alive. We render only the front of the buffer.

    int numberAlive;

    int numberParticles;

 

private:

    ParticleData(const ParticleData&rhs);

    ParticleData& operator=(const ParticleData&rhs);

 

    void initialize(size_t maxsize, int attributes);

};

 

ParticleData成员变量表示粒子的每个属性数据。目前包括位置(position)、速度(velocity)、加速度(acceleration)、颜色(color)、年龄(age)和大小(size)。每个属性数据用数组表示以遵守粒子系统设计原则2。另外两个成员变量numberAlivenumberParticles分别表示当前存活(被渲染)的粒子数目和所有粒子的数目。

 

虚拟基类ParticleSystem描述了粒子系统,其定义如下:

class ParticleSystem

{

public:

    ParticleSystem();

    ~ParticleSystem();

 

    virtual void Generate(int particleCount) = 0;

    virtual void Update(float timeTotal, float timeDelta, float4x4 model, float4x4 view, float4x4 projection) = 0;

    void Render() { if ( renderer ) renderer->Render(); }

 

    void SetGenerator(ParticleGenerator *pGenerator) { generator = pGenerator; }

    void SetUpdator(ParticleUpdator *pUpdator) { updator = pUpdator; }

    void SetRenderer(ParticleRenderer *pRenderer) { renderer = pRenderer; }

 

protected:

    ParticleData particles;

    ParticleGenerator *generator;

    ParticleUpdator *updator;

    ParticleRenderer *renderer;

};

 

ParticleSystem声明了连个纯虚拟函数GenerateUpdate,它们分别用于生成和更新粒子数据。具体的生成和更新方法在子类中实现。与GenerateUpdate相比,函数Render没有声明为虚拟函数,因为Render的实现由类成员renderer实现。

类成员renderer的类型为ParticleRender。将渲染功能从类ParticleSystem中独立出来是为了让数据渲染和管理相独立,遵循SRP原则[16]

在示例程序中,我们实现类ParticleSystem的三个子类:ParticleSystemSpray, ParticleSystemSprite, ParticleSystemStreamline,分别实现礼花、爆炸、风三个效果。类ParticleRender有两个直接子类:ParticleRenderGLParticleRenderD3D,这两个类又根据不同的效果成为其他子类的父类。如ParticleRenderGL又有三个子类ParticleRenderGLSpray, ParticleRenderGLSprite, ParticleRenderGLStreamline

6.5.    粒子系统渲染方法的实现

DirectX的示例程序实现了geometry shaderinstancing两种方法。如果系统支持DirectX 10,那么程序选择geometry shader方法,否则选择instancing方法。OpenGL的示例程序只实现了instancing方法。今后程序升级时我们将实现更多的方法。

 

6.6.    风场流矢的实现

风场流矢的实现过程如下:

,从二维经纬度网格的风场计算出流线和速度。

二,从一条流线上选择连续的若干个(至少应该3个)点,作为粒子的集合顶点,根据这条流线上的速度确定粒子的颜色。

三,粒子的运动通过改变其顶点位置实现。更新顶点位置可以通过找到该粒子所在的流线,选择当前点后面的点。当到达流线的末尾时,再回到开始,从流线起点处选择若干点。

下图显示了按照上述步骤实现的风场流矢粒子系统,显示效果很差。

2:未做效果优化的风场流矢粒子系统。

 

为了能够得到像[16][17]那样的显示效果,我们需要做出一个改进,主要从以下几个方面:

第一,适当增加粒子的长度。图2中的粒子比较短。一种方法是在shader中改变粒子顶点的坐标。

第二,改变粒子的形状。图2中粒子是矩形,可以改为楔形[17]或者椭圆[18]

第三,改变粒子的颜色,从纯色填充改为线性渐变填充。即头部亮度为0,尾部亮度为1

第四,可以考虑使用描绘粒子形状的纹理图片。使用纹理图片的好处有三点:第一,绘制纹理可以去掉图2中的锯齿现象。第二,可以通过纹理中的Alpha层确定粒子的形状。第三,通过纹理中的RGB层确定粒子的亮度。

 

3:经过上述四步效果优化的风场流矢粒子系统。

 

示例程序下载

Windows 商店版本(XAML+DirectX 11):

http://www.eastmodelsoft.com/blog/2016/particlesystem/PSDemoDX20160112.zip

桌面版本(GLUT+OpenGL 3.3):

http://www.eastmodelsoft.com/blog/2016/particlesystem/PSDemoGL20160114.zip

iOS版本(Object C+OpenGL ES 2):

http://www.eastmodelsoft.com/blog/2016/particlesystem/PSDemoiOS20160206.zip

 

参考文献

[1] Real-Time Rendering, third edition.

[2] Reeves, William T., Particle Systems – A Technique for Modeling a Class of Fuzzy Objects, ACM Transactions on Graphics, vol 2, no. 2, pp. 91-108, April 1983.

[3] Particle Systems.

http://www.paraschopra.com/tutorials/particle-systems/

[4] Flexible particle system - OpenGL Renderer

http://www.codeproject.com/Articles/795065/Flexible-particle-system-OpenGL-Renderer

[5] Building an Advanced Particle System

http://www.gamasutra.com/view/feature/131565/building_an_advanced_particle_.php

[6] Introduction to 3D Game Programming with DirectX 11.

http://www.d3dcoder.net

[7] Coding4Fun FallFury

http://fallfury.codeplex.com

[8] Rastertek Tutorial. Tutorial 39: Particle Systems

http://www.rastertek.com/dx11tut39.html

[9] OpenGL ES Particle System Tutorial

http://www.raywenderlich.com/37600/opengl-es-particle-system-tutorial-part-1

[10] Particles | Instancing

http://www.opengl-tutorial.org/intermediate-tutorials/billboards-particles/particles-instancing/

[11] Buffer Object Streaming

http://www.opengl.org/wiki/Buffer_Object_Streaming.

[12] OpenGL Programming Guide, 8th edition.

[13] The OpenGL Extension Wrangler Library.

http://glew.sourceforge.net/

[14] GLUT - The OpenGL Utility Toolkit.

https://www.opengl.org/resources/libraries/glut/.

[15] CMake

https://www.cmake.org.

[16] Single responsibility principle

https://en.wikipedia.org/wiki/Single_responsibility_principle.

[17] Windyty, wind map & forecast

https://www.windyty.com.

[18] MeteoEarth.com - Interactive 3D globe brings weather to life.

http://www.meteoearth.com/