作者:favoyang
粒子系统在图形学上的应用十分广泛。最常见的,是通过控制大量的、具有相似行为 的点元素,来描绘自然现象。例如下雨、下雪、火焰、水、雾等等。在家用控制台或PC上的2D、3D游戏中,粒子特效代替了更为传统的精灵绘图,并取得了更 灵活多变的视觉效果。但在手机设备上,因为机能和API的诸多限制,对于这一技术的应用并不常见。本文试图尝试一下如何在这些设备上实现简单的例子系统。 这仅仅是一次试验,并不能保证一定能用于你的实际开发。
粒子系统简介
粒子系统,即粒子群系统。在自然界中,微粒以各种各 样的方式聚集在一起形成,拥有了自身的属性和运动规律,构成了复杂的世界。对这种微粒的研究过于宽泛,我们仅仅关注微粒系统在图形学上应用的一小部分应 用。一个常见的微粒系统由两部分组成:微粒群和微粒管理器。微粒包括了一系列的属性:速度、颜色、贴图等和一系列的行为。微粒管理器则负责创建微粒群,调 用微粒群的行为、以及将微粒群表现出来(显示)。
例子系统常见的应用有:描述群体(一个子弹群)、描述自然现象(下雨、下雪、火焰、水、雾)。此文关注的是后者。
实现粒子系统的成本
实现通用粒子系统的前提:
1,要有足够的内存空间保存粒子系统本身
2,可以对屏幕上的点进行控制
对于第1条,我的建议是,不要为每一个粒子系统建立类对象,仅仅保存一个多维数组用作描述状态信息。行为可以抽象到粒子管理器去处理。尽管这样,当粒子数目很多时其消耗也是非常惊人的。
对于第2条,MIDP2.0提供了对屏幕上的点进行访问的能力。这一能力是通过Graphics的
void drawRGB(int[] rgbData, int offset, int scanlength, int x, int y, int width, int height, boolean processAlpha)
Renders a series of device-independent RGB+transparency values in a specified region.
方
法实现的。此方法不是直接取得对缓冲区的操作能力,而是通过对一个rgbData进行各种操作,然后将它绘画出来,从而实现点的控制。换句话说,对屏幕的
点的控制,转换为了对数组的控制。不开放对缓冲区的直接操作,优点是:不去限制硬件厂商的实现;缺点是:一旦要对整个屏幕进行像素操作,就不得不开辟一个
足够大的数组,则对普通的手机来说显得负担承重。以sek700为例,176*221=38896像素,每个像素一个int(占2Byte),则总共需要
77792B约76KB的连续空间。正如后面看到的,大量的例子算法需要使用到Blur技术用于提高画面效果。此技术需要上次的屏幕的点信息,因此需要两
个76KB的连续空间。这还不算粒子群本身需要的空间。如果仅仅作为屏幕特效,就吃掉两个76KB的连续空间似乎有些得不偿失。另外,标准没有对数组进行
操作的绘图指令,因此如何对数组进行简单的画圆、输出字符等操作都很棘手。所以可以预见,因为标准API的限制,我们仅能有限的利用粒子系统。
试验一:模拟瀑布
基本思路:将粒子群的出生地定于一点,随机产生一个小范围的水平速度。让粒子按照自然规律自由落体。在接触地的时候发生碰撞,产生很小的向上动力(能量衰减)。
以
下是代码,其中blur是一种简单的模糊算法,它可以上画面更见好看,这里使用的是算法是取一个点为其周围四个点的平均值。这个例子使用了调色板技术实现
了瀑布由白向蓝再向黑的过渡(查表法)。通过这个例子,你可了解到例子系统的基本实现、简单的物理模型模拟和调色板的使用技巧。在模拟器上运行很慢,但在
Nokia3310上有13fps。
public class Waterfall {
public static final int X = 0;
public static final int Y = 1;
public static final int VX = 2;
public static final int VY = 3;
public static final int XACC = 4;
public static final int YACC = 5;
public static final int LIFE = 6;
public static final int DECAY = 7;
int[][] particle;
private int max;
private int last;
public Palette pal;
/*
* [...][X] :x-coordinates, [...][Y] :y-coordinates, [...][VX] :x-velocity,
* [...][VY] :y-velocity, [...][XACC] :x-acceleration,
* [...][YACC]:y-acceleration, [...][LIFE] :life cycle, [...][DECAY] :decay
*/
/**
*
*/
public Waterfall(int max) {
this.max = max;
particle = new int[max][8];
pal = new Palette();
initPal();
}
private void initPal() {// 初始化调色板
for (int i = 0; i < pal.data.length; i++) {// black-->blue-->white
pal.setColor(i, i >= 128 ? (i - 128) * 2 : 0, i, i * 2 >= 255 ? 255
: i * 2);
}
// for(int i=0;i<64;i++)
// pal.setColor(i,i,i/3,0);
// for(int i=64;i<192;i++)
// pal.setColor(i,63,21+(i-64)/3,0);
// for(int i=192;i<256;i++)
// pal.setColor(i,63,63,(i-192)*4);
}
public void init(int x, int y, int high) {// 初始化
for (int i = 0; i < max; i++) {
particle[i][LIFE] = 0;// kill all particle first
}
refreshState(x, y);
last = (y + high) << 10;
}
public void refreshState(int x, int y) {// 更新
int xx = x << 10;
int yy = y << 10;
for (int i = 0; i < max; i++) {
if (particle[i][LIFE] <= 0) {
particle[i][X] = xx;
particle[i][Y] = yy;
particle[i][VX] = CoreUtilities.rand(500, 500);
particle[i][VY] = CoreUtilities.rand(500, 1500);
particle[i][XACC] = 0;
particle[i][YACC] = 50;
particle[i][LIFE] = 1000;
particle[i][DECAY] = CoreUtilities.rand(0, 125);
}
}
}
public void move() {// 移动
for (int i = 0; i < max; i++) {
if (particle[i][LIFE] > 0) {
particle[i][X] += particle[i][VX];
particle[i][Y] += particle[i][VY];
// particle[i][VX]+=particle[i][XACC];//not need this time
particle[i][VY] += particle[i][YACC];
particle[i][LIFE] -= particle[i][DECAY];
}
if (particle[i][Y] > last) {
// System.out.println("before: "+particle[i][VY]);
// particle[i][VY] = -particle[i][VY];
// particle[i][DECAY]+=50;
particle[i][LIFE] -= 500;// 模拟能量衰减
// particle[i][VY] += 3500;//manual speed-down
particle[i][VY] = CoreUtilities.rand(-1000, 400);// 模拟能量衰减
// System.out.println("after "+particle[i][VY]);
// particle[i][VX] ;
// particle[i][LIFE] = 0;
// System.out.println("particle[i][LIFE]: "+particle[i][LIFE]);
}
}
}
public void paint(byte[] buffer, int scanlength) {// 绘画
int i = 0;
int col = 0, row = 0;
int lastrow = buffer.length / scanlength - 1;
int lastcol = scanlength - 1;
try {
for (i = 0; i < max; i++) {
col = particle[i][X] >> 10;
row = particle[i][Y] >> 10;
if (row > lastrow || row < 0 || col < 0 || col > lastcol)
continue;
buffer[scanlength * (row > lastrow ? lastrow : row)
+ (col > lastcol ? lastcol : col)] = 127;
}
} catch (Exception e) {
System.out.println(e);
System.out.println("particle[i][X] : " + particle[i][X]);
System.out.println("particle[i][Y] : " + particle[i][Y]);
System.out
.println("particle[i][X]>>10 : " + (particle[i][X] >> 10));
System.out
.println("particle[i][Y]>>10 : " + (particle[i][Y] >> 10));
System.out
.println("scanlength * (row > lastrow ? lastrow : row)+ (col > lastcol ? lastcol : col) :"
+ (scanlength * (row > lastrow ? lastrow : row) + (col > lastcol ? lastcol
: col)));
}
}
public void blur(byte[] inBuffer, byte[] outBuffer, int scanlength) {// 模糊算法
int k = scanlength;
int backheight = inBuffer.length / scanlength;
for (int i = 1; i < backheight - 1; i++) {
for (int j = 0; j < scanlength; j++) {
outBuffer[k] = (byte) ((inBuffer[k - 1] + inBuffer[k + 1]
+ inBuffer[k - scanlength] + inBuffer[k + scanlength]) >> 2);
// if (outBuffer[k] > -126)
// outBuffer[k] -= 2;
// else
// outBuffer[k] = -128;
k++;
}
}
// for (k = 0; k < scanlength; k++) {
// outBuffer[k] = inBuffer[k];
// }
// for (k = (backheight - 1) * scanlength; k < inBuffer.length; k++) {
// outBuffer[k] = inBuffer[k];
// }
}
public String toString() {// 调试信息
StringBuffer sb = new StringBuffer();
for (int i = 0; i < max; i++) {
sb.append(particle[i][X] + " " + particle[i][Y] + " "
+ particle[i][VX] + " " + particle[i][VY] + " "
+ particle[i][XACC] + " " + particle[i][YACC] + " "
+ particle[i][LIFE] + " " + particle[i][DECAY] + "\n");
}
return sb.toString();
}
}
试验二:模拟定点火
有了上面的基础,我们这次就驾轻就熟了。只要准备好调色板,确定好粒子的状态:出生位置,选择适当的blur算法就可以了。
火
焰这个例子有些特殊的地方是,并不需要很多的粒子属性。仅仅将粒子出生于屏幕底端(火焰的定点位置),再通过一个特殊的blur算法,就可实现了。这个
blur算法就是取一点为自身和自身正下方、左下方、右下方四点的平均值。这样一来,火焰就诞生了。这个例子在Nokia3310上有13fps。因为选
取的调色板不是很好,所以颜色很黯淡,你也可以使用更高的调色板达到更好的效果。
试验三:模拟爆炸
和火焰大同小异,blur算法换回了标准的算法。主要是物理模型变动,加速度很重要,因为它可以改变物体的运动方向。这个例子在Nokia3310上有13fps。
试验四:模拟水波
算法来自一片经典文章,是某位前辈很早之前贡献出来的,有兴趣可以google一下。为表尊重,图片依旧用的原来的图片。算法大意是通过势能计算物体位移。这个例子在Nokia3310上有12fps。
试验五:模拟星空
最后这个例子并没有用到blur算法,所以速度很快,效果也不错。只要是利用颜色速度区分远近达到视觉上的层次感效果。这个例子在Nokia3310上有36fps。
总结
机 能和标准API的双重限制,让我们使用粒子系统遇到了不少的障碍。但在高端手机设备上,内存堆比较大,可以尝试利用粒子系统进行一些特效(菜单使用火焰 等),在支持Alpha合成的机器上,还可以引入一个单纯的特性层,来进行特效合成,这对于高档手机的机能是个开发。本文仅仅泛泛而谈,几个实例也很简 单,都有优化的余地。感兴趣读者可自行实现。