Android粒子系统库——DroidParticle
今天给大家介绍一款粒子系统库,并简要介绍下粒子系统的工作原理。
首先这款名为DroidParticle的库其实就是我自己没事做的,因为以前看过HGE的C++的粒子系统,觉得很有趣,现在从事Android开发工作就模仿着做了一个,希望对大家有用处。
先给大家看一下效果:
源代码下载地址:
https://github.com/sunty2016/DroidParticle
下面言归正传。
1. 何谓粒子系统
做视觉效果的时候,有时会用到火焰、云雾、光影,而如果用一般贴图的方式制做,则效果比较差。
后来有人便根据这些事物的视觉特性发明了一种方法来模拟:将同一张很简单的图片不断地贴到画布上,每次贴上去的图片都只存在一小段时间,而在这段时间内它的位置,大小,颜色,透明度会发生变化。当大量的贴图诞生、变化、死亡,画布上便会呈现出与源图片完全不同的效果。比如说上面第二张火焰图完全是由一张画着字母“Z”的图片贴图而成的,读者们能看出来么?
粒子系统便是这种方法的软件实现,其中每个贴到画布进行变化的贴图叫做粒子。
目前许多绘图、建模工具支持粒子系统的制作。而本文介绍的DroidParticle则是用编程的方式在Android应用中直接实现的粒子系统。
2. DroidParticle的实现
要实现粒子系统,则“粒子系统”和“粒子”是两个很好识别出的类
其中表示粒子的类代码如下:
public class Particle {
public static class Inter {
public float get(float v0, float v1, float t) {
return v0 + (v1 - v0) * t;
}
}
public static class Var {
public Var() {
inter = new Inter();
}
public void set(float v0, float v1) {
this.v0 = v0;
this.v1 = v1;
this.vt = v0;
}
public void set(Var other) {
this.v0 = other.v0;
this.v1 = other.v1;
this.vt = other.vt;
}
public void update(float t) {
this.vt = inter.get(v0, v1, t);
}
public float v0;
public float v1;
public float vt;
public Inter inter;
}
public Particle() {
velo__ = new Var();
bias__ = new Var();
spin__ = new Var();
scale__ = new Var();
alpha__ = new Var();
red__ = new Var();
green__ = new Var();
blue__ = new Var();
matrix = new Matrix();
colorMatrix = new ColorMatrix();
colorFilter = new ColorMatrixColorFilter(colorMatrix);
}
public long durInMillis;
public long timeInMillis;
public long startTimeInMillis;
public float x, y; // pixel
public float theta; // degree
public float rot; // degeee
public float scale; // 1.0
public float alpha; // 1.0
public float red; // 1.0
public float green; // 1.0
public float blue; // 1.0
public Bitmap bitmap;
public Matrix matrix;
public ColorMatrix colorMatrix;
public ColorMatrixColorFilter colorFilter;
public boolean deleteMark;
public Var velo__; // pixel per second
public Var bias__; // degree per second
public Var spin__; // degree per second
public Var scale__; // 1.0
public Var alpha__; // 1.0
public Var red__; // 1.0
public Var green__; // 1.0
public Var blue__; // 1.0
static private final double PI_D_180 = Math.PI / 180.0;
public void config(Bitmap image, int x, int y, ParticleSystemConfig config, Random random) {
this.bitmap = image;
this.x = x;
this.y = y;
config.setParticle(random, this);
}
public void reset(long timeInMillis) {
this.startTimeInMillis = timeInMillis;
this.timeInMillis = timeInMillis;
this.deleteMark = false;
}
public boolean update(long timeInMillis) {
matrix.reset();
matrix.postTranslate(-bitmap.getWidth() / 2, -bitmap.getHeight() / 2);
matrix.postScale(scale, scale);
matrix.postRotate(rot);
matrix.postTranslate(x, y);
colorMatrix.reset();
colorMatrix.setScale(red, green, blue, alpha);
ColorMatrixFilterHelper.setFilterMatrix(
colorFilter,
colorMatrix);
if (this.timeInMillis - this.startTimeInMillis >= this.durInMillis) {
return true;
}
double dtInSeconds = (double)(timeInMillis - this.timeInMillis) * 0.001;
double rad = theta * PI_D_180;
double dd = velo__.vt * dtInSeconds;
//Log.i("STY", String.format("velo__.vt %f, dt %f, dd %f", velo__.vt, dt, dd));
x += dd * Math.cos(rad);
y += dd * Math.sin(rad);
theta += bias__.vt * dtInSeconds;
rot += spin__.vt * dtInSeconds;
scale = scale__.vt;
alpha = alpha__.vt;
red = red__.vt;
green = green__.vt;
blue = blue__.vt;
this.timeInMillis = timeInMillis;
float t = (float)(this.timeInMillis - this.startTimeInMillis) / (float)this.durInMillis;
velo__.update(t);
bias__.update(t);
spin__.update(t);
scale__.update(t);
alpha__.update(t);
red__.update(t);
green__.update(t);
blue__.update(t);
return false;
}
public void draw(Canvas canvas, Paint paint) {
paint.setColorFilter(colorFilter);
canvas.drawBitmap(bitmap, matrix, paint);
}
}
Particle类中,Var类型的成员表示在粒子生命周期中需要变化的参数,Var的v0表示起始值,v1表示结束值,vt表示当前值:
- velo__: 移动速度大小
- bias__: 前进路线的偏移(角)速度(比如开始让粒子沿X轴前进,1秒后沿Y轴前进,则该值设为90)
- spin__: 自旋角速度
- scale__: 缩放
- alpha__: 透明度
- red__, green__, blue__: RGB三色占比
- x, y: 位置
- theta: 移动方向(角度)
- rot: 自旋角度
- scale,alpha,red,green,blue: 参考Var成员
当一个Particle对象先被调用config来设置这些参数,然后没隔一小段时间调一次update和draw。在update中先由当前状态变量更新matrix和colorMatrix,再由Var变量更新当前状态变量;在draw中则应用matrix和colorMatrix最终绘图。
实现粒子系统的类中主要有两处重要的地方,一个是粒子的更新相关代码,另一个是生产粒子的代码:
final private Runnable mUpdatePost = new Runnable() {
@Override
public void run() {
mSimpleTimer.mark();
if (mState != STATE_BUSY && mState != STATE_STOPPING) {
return;
}
long time = System.currentTimeMillis();
++mFrameCount;
synchronized (mParticles) {
updatePtc(time);
if (mState == STATE_STOPPING && mParticles.size() == 0) {
mState = STATE_READY;
if (mOnStateChangeListener != null) {
mOnStateChangeListener.onStateChanged(STATE_READY, STATE_STOPPING);
}
}
}
mParticleSystemView.postInvalidate();
long cost = mSimpleTimer.mark();
mHandler.postDelayed(this, mDpf - cost);
}
};
private void updatePtc(long time) {
for (int i = 0; i < mParticles.size(); ++i) {
Particle ptc = mParticles.removeFirst();
if (ptc.deleteMark) {
ParticlePool.get().recycle(ptc);
} else {
ptc.deleteMark = ptc.update(time);
mParticles.addLast(ptc);
}
}
if (mNewBlend != mBlend) {
mBlend = mNewBlend;
if (mBlend == 0) {
mPaint.setXfermode(null);
} else if (mBlend == 1) {
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.ADD));
}
}
}
final private Runnable mSpawnPost = new Runnable() {
@Override
public void run() {
mSimpleTimer.mark();
if (mState != STATE_BUSY) {
return;
}
long time = System.currentTimeMillis();
if (mPtcImage != null) {
++mPtcCount;
Particle ptc = ParticlePool.get().obtain();
ptc.config(mPtcImage, mX, mY, mConfig, mRandom);
ptc.reset(time);
ptc.update(time);
synchronized (mParticles) {
mParticles.push(ptc);
}
}
long cost = mSimpleTimer.mark();
mHandler.postDelayed(this, mDpp - cost);
}
};
这里的实现主要注意了以下几个问题:
- 粒子更新的速度和粒子产生的速度并不一致。mDpf表示delay per frame,而mDpp表示delay per particle。
- 由于在此处代码要被大量地调用,根据Google的性能建议,这里应该尽量少new新对象,以避免GC被频繁调用。因此对Particle采取obtain/recycle的模式进行复用。相信Android自己的Parcel和MotionEvent等也是出于相同的考虑。
- 基于同样的原因,内部的循环不使用“for (Particle ptc : mParticles) {}”这类写法,以避免产生大量隐蔽的enumerator对象
- 这里mParticles被同步保护的原因是,View会在主线程中访问mParticles并将其中的粒子逐个画出,而这里的mHandler是另外一个HandlerThread的Handler。
除Particle类和ParticleSystem类外,ParticleSystemView类也是重要的类,正是它使粒子系统被画在Android应用里。
ParticleSystemView直接继承自View,可用在layout的资源文件中。
3. DroidParticle的使用
使用方式非常简单,直接给代码:
public class MainActivity extends Activity {
ParticleSystemView mPtcSysView;
ParticleSystem mPtcSys1;
ParticleSystem mPtcSys2;
ParticleSystem mPtcSys3;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mPtcSysView = (ParticleSystemView) findViewById(R.id.canvas);
mPtcSys1 = mPtcSysView.createParticleSystem();
mPtcSys2 = mPtcSysView.createParticleSystem();
mPtcSys3 = mPtcSysView.createParticleSystem();
BitmapDrawable drawable = (BitmapDrawable) getResources().getDrawable(R.drawable.ptc16);
Bitmap img = drawable.getBitmap();
mPtcSys1.setPtcBlend(1);
mPtcSys1.setFps(40);
mPtcSys1.setPps(30);
mPtcSys1.setPtcImage(img);
mPtcSys1.setConfig(createConfig(1));
mPtcSys2.setPtcBlend(1);
mPtcSys2.setFps(40);
mPtcSys2.setPps(30);
mPtcSys2.setPtcImage(img);
mPtcSys2.setConfig(createConfig(2));
mPtcSys3.setPtcBlend(1);
mPtcSys3.setFps(40);
mPtcSys3.setPps(30);
mPtcSys3.setPtcImage(img);
mPtcSys3.setConfig(createConfig(3));
mPtcSysView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
v.removeOnLayoutChangeListener(this);
mPtcSys1.setPtcPosition(v.getWidth() / 6, v.getHeight() / 2);
mPtcSys2.setPtcPosition(v.getWidth() / 2, v.getHeight() / 2);
mPtcSys3.setPtcPosition(v.getWidth() * 5 / 6, v.getHeight() / 2);
mPtcSys1.start();
mPtcSys2.start();
mPtcSys3.start();
}
});
}
@Override
protected void onDestroy() {
super.onDestroy();
mPtcSysView.releaseParticleSystem(mPtcSys1);
mPtcSysView.releaseParticleSystem(mPtcSys2);
mPtcSysView.releaseParticleSystem(mPtcSys3);
}
static private ParticleSystemConfig createConfig(int id) {
ParticleSystemConfig config = new ParticleSystemConfig();
config.duration.set(1000, 0);
config.theta.set(270, 15);
config.startVelocity.set(400, 0);
config.endVelocity.set(400, 0);
config.startAngularRate.set(0, 0);
config.endAngularRate.set(0, 0);
config.startSpinRate.set(360, 0);
config.endSpinRate.set(360, 0);
config.startScale.set(1, 0);
config.endScale.set(1.5f, 0);
config.startAlpha.set(1, 0);
config.endAlpha.set(0.75f, 0);
if (id == 1) {
config.startRed.set(1, 0);
config.endRed.set(1, 0);
config.startGreen.set(0, 0);
config.endGreen.set(1, 0);
config.startBlue.set(0, 0);
config.endBlue.set(0, 0);
} else if (id == 2) {
config.startRed.set(0, 0);
config.endRed.set(0, 0);
config.startGreen.set(1, 0);
config.endGreen.set(1, 0);
config.startBlue.set(0, 0);
config.endBlue.set(1, 0);
} else if (id == 3) {
config.startRed.set(0, 0);
config.endRed.set(1, 0);
config.startGreen.set(0, 0);
config.endGreen.set(0, 0);
config.startBlue.set(1, 0);
config.endBlue.set(1, 0);
}
return config;
}
}
可以看出同一个ParticleSystemView中可以同时建立多个particle system。但不建议建立太多system,会影响性能。
总结
一般粒子系统的实现都会基于图形库的支持,比如OpenGL等,但是DroidParticle完全是基于Android标准API。这样做的好处是实现、使用起来比较方便,也很轻量级,但缺点就是一来被束缚在2D上,无法扩展为三维粒子库,二来性能上也会差一些。不过就一般的效果而言DroidParticle是完全够用的。
另外GitHub源代码中含有Demo程序,可以帮助读者理解粒子各个参数的作用:
https://github.com/sunty2016/DroidParticle