Android粒子系统库——DroidParticle

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三色占比
而非Var成员则保存当前的一些状态,节省计算成本:
  • 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


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值