10.2 粒子系统技术
3D粒子系统可以产生各种各样的自然效果,像烟、火、闪光灯,也可以产生随机的和高科技风格的图形效果。可以说,粒子系统是一类令人激动又十分有趣的动画程序。它的实现方式主要需要用基于粒子系统构建的图形学、动力学以及数字艺术等多方面的知识。
10.2.1 粒子系统简介
粒子系统主要用来实现物理模拟,比如自由落体、星空、爆炸等,或某些自然效果,比如烟雨、瀑布等。粒子系统是一些粒子的集合。它通过指定发射源,在发射粒子流的同时创建各种动画效果。
在本章的代码中,粒子系统是一个对象,而发射的粒子是粒子对象,并且随时间调整粒子的属性,以控制粒子行为,然后将粒子系统作为一个整体进行绘制。
粒子系统是一个相对独立的造型系统,用来创建粒子物体模拟雨、雪、灰尘、泡沫、火花和气流等。采用纹理的粒子系统可以将任何造型作为粒子,所以其表现能力也大大增强,例如可以制作成群的蚂蚁、游动的热带鱼群、吹散飞舞的蒲公英等。
粒子系统主要用于表现动态的效果,与时间、速度的关系非常紧密,一般用于动画制作。
10.2.2 粒子系统应用
经过初步总结,粒子系统常常用来表现下面的特殊效果。
n 雨雪:使用喷射和暴风雪粒子系统,可以创建各种雨景和雪景,在加入Wind风力的影响后可制作斜风细雨和狂风暴雪的景象。
n 泡沫:可以创建各种气泡、水泡效果。
n 爆炸和礼花:如果将一个3D造型作为发散器,粒子系统可以将它炸成碎片,加入特殊的材质和合成特技就可以制作成美丽的礼花。
n 群体效果:Blizzard(暴风雪)、PArray(粒子阵列)、PCloud(粒子云)和Super Spray(超级喷射)这4种粒子系统都可以用3D造型作为粒子,因此可以表现出群体效果,如人群、马队、飞蝗和乱箭等。
10.2.3 粒子系统属性
粒子系统除自身特性外,还有一些共同的属性,这些属性并不一定要划分明确,有时是在同一个类中设定的。
n Emitter(发射属性):用于发射粒子。所有的粒子都由它喷出,它的设置决定了粒子发射时的位置、面积和方向。Emitter在视图中显示为黄色,不可以被渲染。
n Timing(衰减属性):控制粒子的时间参数,包括粒子产生和消失的时间、粒子存在的时间或寿命、粒子的流动速度以及加速度。
n Particle-Specific Parameters(指定粒子参数):控制粒子的尺寸、速度,不同的粒子系统,其设置也不相同。
n Rendering Properties(渲染特性):控制粒子在视图中、渲染时和动画中分别表现出的形态。由于粒子显示不易,所以通常以简单的点、线或交叉点来显示,而且数目也只用于操作观察之用,不用设置过多。对于渲染效果,它会按真实指定的粒子类型和数目进行着色计算。
10.2.4 粒子系统模型
粒子系统可以说是一种基于物理模型来解决问题的方法,它的核心不是在于如何显示而是在于对粒子运行规则的提取,粒子算法才是整个系统的精华所在。一个粒子运行过程如图10-13所示,图10-14为粒子在喷泉中的应用。

图10-13 粒子的运行过程 图10-14 粒子的喷泉应用
10.2.5 焰火粒子系统
下面将构造一个简单的焰火粒子系统。整个系统由3个类组成:Particle、FireworksEffect和ParticleSystem。它们在整个系统中的作用和逻辑关系如图10-15所示。
Particle类存储了每个粒子的基本属性,包括生命、衰减、速度、位置和颜色,对于复杂的粒子还包含其他更多属性。Particle类的代码如下:
public class Particle
{
private float life = 1.0f; //生命
private float degradation = 0.1f; //衰减
private float[] vel = {0.0f, 0.0f, 0.0f}; //速度
private float[] pos = {0.0f, 0.0f, 0.0f}; //位置
private int color = 0xffffff; //颜色
public Particle(){}
public Particle(float[] velocity, float[] position, int color)
{
setVel(velocity); //设置初始速度
setPos(position); //设置初始位置
this.setColor(color); //设置初始颜色
}
void setLife(float life) { //设置生命
this.life = life;
}
float getLife() { //获取生命
return life;
}
void setVel(float[] tvel) { //设置速度
System.arraycopy(tvel, 0, vel, 0, vel.length);
}
float[] getVel() { //获取速度
return vel;
}
void setPos(float[] tpos) { //设置位置
System.arraycopy(tpos, 0, pos, 0, pos.length);
}
float[] getPos() { //获取位置
return pos;
}
void setColor(int color) { //设置颜色
this.color = color;
}
int getColor() { //获取颜色
return color;
}
public void setDegradation(float degradation) { //设置衰减
this.degradation = degradation;
}
public float getDegradation() { //获取衰减
return degradation;
}
}
代码中的arraycopy用于复制数组。该方法认为数组是一种比较特殊的object,该复制方法比逐个赋值效率要高。该方法原型如下:
public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);
FireworksEffect类提供了init方法对粒子的参数进行初始化。代码如下:
private float[] pos = {0.0f, 0.0f, 0.0f}; //初始位置(发射位置)
Random rand = new Random(); //随机数
public void init(Particle p)
{
p.setLife(1.0f); //设置粒子的生命
p.setPos(pos); //设置粒子的位置
float[] vel = new float[3]; //速度
float xyvel = rand.nextFloat() * 0.8f + 0.2f;
//rand.nextFloat()随机生成0~1的数
p.setDegradation(xyvel / 18); //设置衰减速度
vel[0] = xyvel * trig[1] + rand.nextFloat() * 0.125f - 0.0625f; //x方向速度
vel[1] = xyvel * trig[0] + rand.nextFloat() * 0.125f - 0.0625f; //y方向速度
vel[2] = 0.0f; //z方向速度
p.setVel(vel); //设置粒子的速度数组
int r = (int)(120 * rand.nextFloat()) + 120; //随机生成Red颜色
int g = (int)(120 * rand.nextFloat()) + 120; //随机生成Green颜色
int b = (int)(120 * rand.nextFloat()) + 120; //随机生成Blue颜色
int col = (r << 16) | (g << 8) | b; //融合RGB
p.setColor(col); //设置粒子的颜色
}
Particle类提供了粒子的数据结构,还需要有粒子实体。这里由FireworksEffect类创建四边形Mesh对象,其顶点结构和纹理坐标如图10-16所示。Mesh对象包含了顶点缓冲、索引缓冲和外观属性,其逻辑结构如图10-17所示。

图10-16 纹理坐标与顶点的对应关系

图10-17 粒子四边形的结构
FireworksEffect类的createAlphaPlane方法创建四边形,该四边形只有位置数组和纹理坐标数组,设置外观属性为只显示正面,并且纹理和色彩进行颜色融合。创建Mesh对象的代码如下:
private Mesh createAlphaPlane(String texFilename)
{
/***************顶点缓冲***************/
short POINTS[] = new short[] {-1, -1, 0,
1, -1, 0,
1, 1, 0,
-1, 1, 0};
VertexArray POSITION_ARRAY = new VertexArray(POINTS.length/3, 3, 2);
//顶点数组
POSITION_ARRAY.set(0, POINTS.length/3, POINTS);
short TEXCOORDS[] = new short[] {0, 255,
255, 255,
255, 0,
0, 0};
VertexArray TEXCOORD_ARRAY = new VertexArray(TEXCOORDS.length / 2, 2, 2);
//纹理数组
TEXCOORD_ARRAY.set(0, TEXCOORDS.length / 2, TEXCOORDS);
VertexBuffer vertexBuffer = new VertexBuffer(); //顶点缓冲
vertexBuffer.setPositions(POSITION_ARRAY, 1.0f, null); //设置位置数组
vertexBuffer.setTexCoords(0, TEXCOORD_ARRAY, 1.0f/255.0f, null);
//设置纹理数组
vertexBuffer.setDefaultColor(0xffffffff); //设置默认颜色
/***************索引缓冲***************/
int INDICES[] = new int[] {0, 1, 3, 2};
int[] LENGTHS = new int[] {4};
IndexBuffer indexBuffer = new TriangleStripArray(INDICES, LENGTHS);
//索引缓冲
/***************外观属性***************/
Appearance appearance = new Appearance();
PolygonMode polygonmode = new PolygonMode();
polygonmode.setCulling(PolygonMode.CULL_BACK); //剔除背面
appearance.setPolygonMode(polygonmode);
CompositingMode compositingmode = new CompositingMode();
compositingmode.setBlending(CompositingMode.ALPHA); //透明融合
appearance.setCompositingMode(compositingmode);
try{
Image texImage = Image.createImage(texFilename); //加载纹理图片
Texture2D texture = new Texture2D(new Image2D(Image2D.RGBA, texImage));
//支持透明色
texture.setWrapping(Texture2D.WRAP_CLAMP, Texture2D.WRAP_CLAMP);
texture.setFiltering(Texture2D.FILTER_BASE_LEVEL, Texture2D.FILTER_NEAREST);
texture.setBlending(Texture2D.FUNC_BLEND);
appearance.setTexture(0, texture);
}catch(Exception e){ //捕捉异常
System.out.println(e);
}
Mesh mesh = new Mesh(vertexBuffer, indexBuffer, appearance); //创建四边形对象
return mesh;
}
angle指定了焰火喷射的角度,如果是爆炸,粒子发射的角度是全向的。此外还定义了trig数组,trig[0]保存了角度的正弦值,trig[1]保存了角度的余弦值,三角值用来计算速度的x分量和y分量,如图10-18所示。

图10-18 发射方向的计算
设置发射角度和计算三角函数值的代码如下:
private int angle = 90; //发射角度
private float[] trig = {1.0f, 0.0f}; //三角函数
public void setAngle(int angle)
{
this.angle = angle; //设置发射角度
trig[0] = (float)Math.sin(Math.toRadians(angle)); //正弦
trig[1] = (float)Math.cos(Math.toRadians(angle)); //余弦
}
public int getAngle()
{
return angle;
}
在粒子系统中,粒子始终是移动的,在原有位置上加上速度在x和y方向上的偏移量(z方向上不移动),粒子的生命值减去衰减量以判断粒子是否生存,如果已经消逝则重新初始化粒子,更新粒子属性的代码如下:
public void update(Particle p)
{
float[] ppos = p.getPos(); //获取当前位置
float[] vel = p.getVel(); //获取速度
ppos[0] += vel[0]; //x方向上移动
ppos[1] += vel[1]; //y方向上移动
ppos[2] += vel[2]; //z方向上不移动
p.setLife(p.getLife() - p.getDegradation()); //更新生命值
if(p.getLife() < -0.001f) //判断粒子是否存活
{
init(p); //初始化粒子
}
}
在更新好粒子属性后,就可以对粒子进行绘制。粒子的Alpha值随着生命值变化,从而显示逐步消隐的效果。粒子根据scale值进行缩放,并且根据粒子的当前位置对四边形进行平移。代码如下:
float scale = 0.1f; //缩放值
Transform trans = new Transform(); //变换矩阵
public void render(Particle p, Graphics3D g3d)
{
int alpha = (int)(255 * p.getLife()); //Alpha值
int color = p.getColor() | (alpha << 24); //RGBA颜色
mesh.getVertexBuffer().setDefaultColor(color); //设置四边形的颜色
trans.setIdentity();
trans.postScale(scale, scale, scale); //缩放
float[] pos = p.getPos();
trans.postTranslate(pos[0], pos[1], pos[2]); //平移
g3d.render(mesh, trans); //根据变换矩阵绘制四边形
}
FireworksEffect类的构造方法比较简单,主要是设置发射角度,调用createAlphaPlane方法创建四边形粒子,代码如下:
public FireworksEffect(int angle)
{
setAngle(angle); //设置发射角度
mesh = createAlphaPlane("/particle.png"); //创建四边形
}
Particle类保存了粒子的方位速度等抽象信息,FireworksEffect类用来创建、更新粒子,ParticleSystem类将两者连接并进行管理,该类代码比较简单,主要调用FireworksEffect类的init方法对Particle实例进行初始化,并调用FireworksEffect类的更新和绘制方法,该类是粒子系统的内部管理类,代码如下:
import javax.microedition.m3g.Graphics3D;
public class ParticleSystem
{
private FireworksEffect effect = null; //粒子效果
Particle[] parts = null; //粒子数组
public ParticleSystem(FireworksEffect effect, int num)
{
setEffect(effect); //设置粒子效果
parts = new Particle[num]; //根据数量创建粒子数组
for(int i = 0; i < num; i++)
{
parts[i] = new Particle();
effect.init(parts[i]); //根据粒子效果的内容初始化粒子数组,部分数据采用随机生成
}
}
public void emit(Graphics3D g3d)
{
for(int i = 0; i < parts.length; i++)
{
getEffect().update(parts[i]); //更新粒子信息
getEffect().render(parts[i], g3d); //绘制粒子
}
}
public void setEffect(FireworksEffect effect) {
this.effect = effect;
}
public FireworksEffect getEffect() {
return effect;
}
}
在游戏画布的run方法中将接收键盘事件,并根据键盘输入调整粒子的发射角度。
public void run() {
while(true) {
try {
int keys = getKeyStates(); //获取键盘输入
if((keys & GameCanvas.LEFT_PRESSED) != 0) //左键按下
key[LEFT] = true;
else
key[LEFT] = false;
if((keys & GameCanvas.RIGHT_PRESSED) != 0) //右键按下
key[RIGHT] = true;
else
key[RIGHT] = false;
try{
g3d.bindTarget(g, true, Graphics3D.ANTIALIAS |
Graphics3D.TRUE_COLOR | Graphics3D.DITHER);
g3d.clear(back); //用背景对象清空深度缓冲
g3d.setCamera(camera, identity); //设置摄影机
if(ps == null)
{
effect = new FireworksEffect(90);
//创建粒子效果对象,发射角度为90°,正上方
ps = new ParticleSystem(effect, 30); //创建包含30个粒子的粒子系统
}
ps.emit(g3d); //更新粒子,并绘制粒子
if(key[LEFT])
effect.setAngle(effect.getAngle() + 5); //更改粒子的发射角度
if(key[RIGHT])
effect.setAngle(effect.getAngle() - 5);
}catch(Exception e){
e.printStackTrace();
}finally{
g3d.releaseTarget();
}
flushGraphics(); //刷新屏幕
try{ Thread.sleep(30); } catch(Exception e) {} //休眠30ms
}catch(Exception e) {
e.printStackTrace();
}
}
}
编译、运行程序,其结果如图10-19所示。

图10-19 焰火粒子系统
10.2.6 爆炸粒子系统
游戏中经常使用到爆炸特效,爆炸可以使用单纯的图片回放,也可以用程序仿真爆炸的物理过程,这样爆炸效果更加逼真。使用粒子系统对爆炸进行模拟的基本原理是用大量粒子对爆炸的流体力学原理进行模拟。
在爆炸效果的运动过程中,一开始它是将各种块状物体在一瞬间由一个圆心向外围圆周的任意方向拓展,形成一个爆炸最初期的现象,如图10-20所示。

图10-20 圆形爆炸示意
要营造爆炸粒子系统,可以在上面的焰火喷射的基础上加以改动,将粒子向单一方向发射改为向四面八方喷射,这点可以通过改变例子在x方向和y方向上的增量实现。
在ParticleSystem类的构造方法中,将根据粒子的数量对360°圆周角进行划分,并在粒子效果初始化粒子时,将该角度传递给初始化方法,当然也可以根据粒子数目在360°内随机生成发射角度。代码如下:
public ParticleSystem(ExplosionEffect effect, int num)
{
setEffect(effect);
parts = new Particle[num]; //根据指定个数构造粒子数组
for(int i = 0; i < num; i++)
{
parts[i] = new Particle();
effect.init(parts[i],360*i/num); //初始化粒子,并计算粒子的发射角度
}
}
在知道发射角度之后,x和y方向的偏移量就可以依次推算出,爆炸过程如果过于规则,会显得不真实,因此还增加了一些随机效果,x和y方向上的速度代码如下:
vel[0] = xyvel * (float)Math.cos(Math.toRadians(angle)) + rand.nextFloat() * 0.125f - 0.0625f;
vel[1] = xyvel * (float)Math.sin(Math.toRadians(angle)) + rand.nextFloat() * 0.125f - 0.0625f;
在其他代码不变的情况下,运行程序,粒子向四面八方发射,模拟了实际中的爆炸效果,如图10-21所示。
现在已经可以做出控制粒子爆炸运动的基本模型,除此之外,可以在每一个粒子运动的过程中加上一个产生加速度的力,而这个力在爆炸瞬间的力量是最大的,所以粒子的加速度会由快变到慢。

图10-21 粒子系统的爆炸效果
重力在空间中是一个向下的作用力,所以当粒子在运动的同时,还可以再加上一个向下的力。读者可以进一步完善这个爆炸模型,以便达到更好的效果。
一般来说,爆炸时的颜色变化是非常迅速的,很多细微的变化都觉察不到,不过为了效果更加逼真,尽量采用比较完整的颜色过渡值来模拟。
考虑到手机设备的局限性,由于手机的屏幕不是很大,游戏中所要涉及到的爆炸效果不会是大面积的爆炸,也就是说,完全可以忽略一些细节,最终的效果看起来比较逼真就是主要目的,细节并不是重点。这里笔者选择图10-22所列出的颜色值来组成粒子的调色板,调色板中的颜色是按从黑到红再到白的顺序排列。
对照调色板颜色,构造了一个颜色数组,代码如下:
private int[] color =
{0x000000,0x100000,0x2C0000,0x480000,0x650000,0x810000, 0xA10000,0xC20000,
0xE20000,0xFF0000,0xFF6500,0xFF9500,0xFFC600,0xFFFF00, 0xFFFF7D,0xFFFFFF};
要使粒子颜色发生变化,最简单的方法是每更新一次粒子位置就让它的颜色值也从后向前移一位,也就是说,粒子每更新一次状态,颜色值也由白向黑的方向变化一个色度,即调色板中的颜色索引值减1。
在Particle类中增加color_index变量作为颜色索引,初始值为15,在ExplosionEffect粒子效果类中,更新粒子状态的update(Particle p)方法中增加代码片断如下:
int color_index = p.getColorIndex(); //获取粒子颜色
if(p.getLife()>0)p.setColor(color[color_index]); //根据颜色数组中的内容设定粒子颜色
color_index -= 1; //每更新一次,索引递减
if(color_index<=0)color_index=0;
p.setColorIndex(color_index); //根据索引设置粒子的颜色
编译运行程序,其结果如图10-23所示,粒子有了向下的速度,颜色也在不断更新。

图10-23 粒子的颜色变化






